Repository: Codeux/Textual Branch: master Commit: 243a6e2c06ad Files: 910 Total size: 7.4 MB Directory structure: gitextract_u26s7vme/ ├── .gitignore ├── .gitmodules ├── Common/ │ ├── Documents/ │ │ └── Acknowledgements.docx │ └── Release DMG.dmgCanvas/ │ └── Disk Image ├── Configurations/ │ ├── Build/ │ │ ├── Code Signing Identity.xcconfig │ │ ├── Common/ │ │ │ ├── Foundation Debug.xcconfig │ │ │ ├── Foundation.xcconfig │ │ │ ├── Preserve Symbols.xcconfig │ │ │ ├── Textual App.xcconfig │ │ │ ├── Textual Extensions.xcconfig │ │ │ ├── Textual.xcconfig │ │ │ ├── XPC Service - ICL Extensions.xcconfig │ │ │ ├── XPC Service - ICL.xcconfig │ │ │ └── XPC Services.xcconfig │ │ ├── Debug/ │ │ │ ├── Enabled Features.xcconfig │ │ │ ├── Textual App.xcconfig │ │ │ ├── Textual Extensions.xcconfig │ │ │ ├── Textual.xcconfig │ │ │ ├── XPC Service - ICL Extensions.xcconfig │ │ │ ├── XPC Service - ICL.xcconfig │ │ │ └── XPC Services.xcconfig │ │ ├── README.md │ │ └── Standard Release/ │ │ ├── Enabled Features.xcconfig │ │ ├── Textual App.xcconfig │ │ ├── Textual Extensions.xcconfig │ │ ├── Textual.xcconfig │ │ ├── XPC Service - ICL Extensions.xcconfig │ │ ├── XPC Service - ICL.xcconfig │ │ └── XPC Services.xcconfig │ ├── ExportArchiveConfiguration.plist │ └── Sandbox/ │ └── Inherited.entitlements ├── README.md ├── Sources/ │ ├── App/ │ │ ├── Build Scripts/ │ │ │ ├── BuildExtensions.sh │ │ │ ├── BuildFrameworks.sh │ │ │ ├── BuildServices.sh │ │ │ ├── ExportArchive.sh │ │ │ ├── MergeSwift.sh │ │ │ ├── PostprocessSparkle.sh │ │ │ ├── UpdateFeatureFlags.sh │ │ │ └── UpdateVersionInfo.sh │ │ ├── Classes/ │ │ │ ├── Controllers/ │ │ │ │ ├── TXAppearance.m │ │ │ │ ├── TXApplication.m │ │ │ │ ├── TXGlobalModels.m │ │ │ │ ├── TXMasterController.m │ │ │ │ ├── TXMenuController.m │ │ │ │ ├── TXSharedApplication.m │ │ │ │ └── TXWindowController.m │ │ │ ├── Dialogs/ │ │ │ │ ├── Channel Spotlight/ │ │ │ │ │ ├── TDCChannelSpotlightAppearance.m │ │ │ │ │ ├── TDCChannelSpotlightController.m │ │ │ │ │ ├── TDCChannelSpotlightControls.m │ │ │ │ │ ├── TDCChannelSpotlightSearchResult.m │ │ │ │ │ └── TDCChannelSpotlightSearchResultsTable.m │ │ │ │ ├── File Transfers/ │ │ │ │ │ ├── TDCFileTransferDialog.m │ │ │ │ │ ├── TDCFileTransferDialogTableCell.m │ │ │ │ │ └── TDCFileTransferDialogTransferController.m │ │ │ │ ├── License Manager/ │ │ │ │ │ └── Standalone/ │ │ │ │ │ ├── TDCLicenseManagerDialog.m │ │ │ │ │ ├── TDCLicenseManagerMigrateAppStoreSheet.m │ │ │ │ │ ├── TDCLicenseManagerRecoverLostLicenseSheet.m │ │ │ │ │ ├── TDCLicenseUpgradeActivateSheet.m │ │ │ │ │ ├── TDCLicenseUpgradeCommonActions.m │ │ │ │ │ ├── TDCLicenseUpgradeDialog.m │ │ │ │ │ └── TDCLicenseUpgradeEligibilitySheet.m │ │ │ │ ├── Preferences/ │ │ │ │ │ ├── TDCPreferencesController.m │ │ │ │ │ ├── TDCPreferencesNotificationConfiguration.m │ │ │ │ │ └── TDCPreferencesUserStyleSheet.m │ │ │ │ ├── Server Endpoint/ │ │ │ │ │ ├── TDCServerEndpointListSheet.m │ │ │ │ │ └── TDCServerEndpointListSheetTable.m │ │ │ │ ├── TDCAboutDialog.m │ │ │ │ ├── TDCAddressBookSheet.m │ │ │ │ ├── TDCAlert.m │ │ │ │ ├── TDCChannelBanListSheet.m │ │ │ │ ├── TDCChannelInviteSheet.m │ │ │ │ ├── TDCChannelModifyModesSheet.m │ │ │ │ ├── TDCChannelModifyTopicSheet.m │ │ │ │ ├── TDCChannelPropertiesNotificationConfiguration.m │ │ │ │ ├── TDCChannelPropertiesSheet.m │ │ │ │ ├── TDCHighlightEntrySheet.m │ │ │ │ ├── TDCInputPrompt.m │ │ │ │ ├── TDCNicknameColorSheet.m │ │ │ │ ├── TDCProgressIndicatorSheet.m │ │ │ │ ├── TDCServerChangeNicknameSheet.m │ │ │ │ ├── TDCServerChannelListDialog.m │ │ │ │ ├── TDCServerHighlightListSheet.m │ │ │ │ ├── TDCServerPropertiesSheet.m │ │ │ │ ├── TDCSheetBase.m │ │ │ │ ├── TDCWelcomeSheet.m │ │ │ │ └── TDCWindowBase.m │ │ │ ├── Headers/ │ │ │ │ ├── External Libraries/ │ │ │ │ │ ├── GTMEncodeHTML.h │ │ │ │ │ └── OELReachability.h │ │ │ │ ├── IRC.h │ │ │ │ ├── IRCAddressBook.h │ │ │ │ ├── IRCAddressBookUserTracking.h │ │ │ │ ├── IRCChannel.h │ │ │ │ ├── IRCChannelConfig.h │ │ │ │ ├── IRCChannelMemberList.h │ │ │ │ ├── IRCChannelMode.h │ │ │ │ ├── IRCChannelUser.h │ │ │ │ ├── IRCClient.h │ │ │ │ ├── IRCClientConfig.h │ │ │ │ ├── IRCColorFormat.h │ │ │ │ ├── IRCCommandIndex.h │ │ │ │ ├── IRCConnection.h │ │ │ │ ├── IRCHighlightLogEntry.h │ │ │ │ ├── IRCHighlightMatchCondition.h │ │ │ │ ├── IRCISupportInfo.h │ │ │ │ ├── IRCMessage.h │ │ │ │ ├── IRCModeInfo.h │ │ │ │ ├── IRCNetworkList.h │ │ │ │ ├── IRCNumerics.h │ │ │ │ ├── IRCPrefix.h │ │ │ │ ├── IRCSendingMessage.h │ │ │ │ ├── IRCServer.h │ │ │ │ ├── IRCTreeItem.h │ │ │ │ ├── IRCUser.h │ │ │ │ ├── IRCUserRelations.h │ │ │ │ ├── IRCWorld.h │ │ │ │ ├── Internal/ │ │ │ │ │ ├── IRCAddressBookInternal.h │ │ │ │ │ ├── IRCChannelConfigInternal.h │ │ │ │ │ ├── IRCChannelUserInternal.h │ │ │ │ │ ├── IRCClientConfigInternal.h │ │ │ │ │ ├── IRCHighlightLogEntryInternal.h │ │ │ │ │ ├── IRCHighlightMatchConditionInternal.h │ │ │ │ │ ├── IRCMessageInternal.h │ │ │ │ │ ├── IRCModeInfoInternal.h │ │ │ │ │ ├── IRCPrefixInternal.h │ │ │ │ │ ├── IRCServerInternal.h │ │ │ │ │ ├── IRCUserInternal.h │ │ │ │ │ ├── TDCChannelPropertiesSheetInternal.h │ │ │ │ │ ├── TDCChannelSpotlightAppearanceInternal.h │ │ │ │ │ ├── TDCChannelSpotlightControllerInternal.h │ │ │ │ │ ├── TDCFileTransferDialogInternal.h │ │ │ │ │ └── TVCLogLineInternal.h │ │ │ │ ├── NSColorHelper.h │ │ │ │ ├── NSStringHelper.h │ │ │ │ ├── NSViewHelper.h │ │ │ │ ├── Private/ │ │ │ │ │ ├── ICLPayloadLocalPrivate.h │ │ │ │ │ ├── IRCAddressBookMatchCachePrivate.h │ │ │ │ │ ├── IRCAddressBookUserTrackingPrivate.h │ │ │ │ │ ├── IRCChannelConfigPrivate.h │ │ │ │ │ ├── IRCChannelMemberListControllerPrivate.h │ │ │ │ │ ├── IRCChannelMemberListPrivate.h │ │ │ │ │ ├── IRCChannelModePrivate.h │ │ │ │ │ ├── IRCChannelPrivate.h │ │ │ │ │ ├── IRCChannelUserPrivate.h │ │ │ │ │ ├── IRCClientConfigPrivate.h │ │ │ │ │ ├── IRCClientPrivate.h │ │ │ │ │ ├── IRCClientRequestedCommandsPrivate.h │ │ │ │ │ ├── IRCColorFormatPrivate.h │ │ │ │ │ ├── IRCCommandIndexPrivate.h │ │ │ │ │ ├── IRCConnectionPrivate.h │ │ │ │ │ ├── IRCExtrasPrivate.h │ │ │ │ │ ├── IRCHighlightLogEntryPrivate.h │ │ │ │ │ ├── IRCISupportInfoPrivate.h │ │ │ │ │ ├── IRCMessageBatchPrivate.h │ │ │ │ │ ├── IRCMessagePrivate.h │ │ │ │ │ ├── IRCServerPrivate.h │ │ │ │ │ ├── IRCTimerCommandPrivate.h │ │ │ │ │ ├── IRCTreeItemPrivate.h │ │ │ │ │ ├── IRCUserNicknameColorStyleGeneratorPrivate.h │ │ │ │ │ ├── IRCUserPersistentStorePrivate.h │ │ │ │ │ ├── IRCUserPrivate.h │ │ │ │ │ ├── IRCUserRelationsPrivate.h │ │ │ │ │ ├── IRCWorldPrivate.h │ │ │ │ │ ├── NSTableVIewHelperPrivate.h │ │ │ │ │ ├── NSViewHelperPrivate.h │ │ │ │ │ ├── SwiftBridgingHeaderPrivate.h │ │ │ │ │ ├── TDCAboutDialogPrivate.h │ │ │ │ │ ├── TDCAddressBookSheetPrivate.h │ │ │ │ │ ├── TDCChannelBanListSheetPrivate.h │ │ │ │ │ ├── TDCChannelInviteSheetPrivate.h │ │ │ │ │ ├── TDCChannelModifyModesSheetPrivate.h │ │ │ │ │ ├── TDCChannelModifyTopicSheetPrivate.h │ │ │ │ │ ├── TDCChannelPropertiesNotificationConfigurationPrivate.h │ │ │ │ │ ├── TDCChannelPropertiesSheetPrivate.h │ │ │ │ │ ├── TDCChannelSpotlightAppearancePrivate.h │ │ │ │ │ ├── TDCChannelSpotlightControllerPrivate.h │ │ │ │ │ ├── TDCChannelSpotlightControlsPrivate.h │ │ │ │ │ ├── TDCChannelSpotlightSearchResultPrivate.h │ │ │ │ │ ├── TDCChannelSpotlightSearchResultsTablePrivate.h │ │ │ │ │ ├── TDCFileTransferDialogPrivate.h │ │ │ │ │ ├── TDCFileTransferDialogTableCellPrivate.h │ │ │ │ │ ├── TDCFileTransferDialogTransferControllerPrivate.h │ │ │ │ │ ├── TDCHighlightEntrySheetPrivate.h │ │ │ │ │ ├── TDCLicenseManagerDialogPrivate.h │ │ │ │ │ ├── TDCLicenseManagerMigrateAppStoreSheetPrivate.h │ │ │ │ │ ├── TDCLicenseManagerRecoverLostLicenseSheetPrivate.h │ │ │ │ │ ├── TDCLicenseUpgradeActivateSheetPrivate.h │ │ │ │ │ ├── TDCLicenseUpgradeCommonActionsPrivate.h │ │ │ │ │ ├── TDCLicenseUpgradeDialogPrivate.h │ │ │ │ │ ├── TDCLicenseUpgradeEligibilitySheetPrivate.h │ │ │ │ │ ├── TDCNicknameColorSheetPrivate.h │ │ │ │ │ ├── TDCPreferencesControllerPrivate.h │ │ │ │ │ ├── TDCPreferencesNotificationConfigurationPrivate.h │ │ │ │ │ ├── TDCPreferencesUserStyleSheetPrivate.h │ │ │ │ │ ├── TDCProgressIndicatorSheetPrivate.h │ │ │ │ │ ├── TDCServerChangeNicknameSheetPrivate.h │ │ │ │ │ ├── TDCServerChannelListDialogPrivate.h │ │ │ │ │ ├── TDCServerEndpointListSheetPrivate.h │ │ │ │ │ ├── TDCServerEndpointListSheetTablePrivate.h │ │ │ │ │ ├── TDCServerHighlightListSheetPrivate.h │ │ │ │ │ ├── TDCServerPropertiesSheetPrivate.h │ │ │ │ │ ├── TDCSharedProtocolDefinitionsPrivate.h │ │ │ │ │ ├── TDCWelcomeSheetPrivate.h │ │ │ │ │ ├── THOPluginDispatcherPrivate.h │ │ │ │ │ ├── THOPluginItemPrivate.h │ │ │ │ │ ├── THOPluginManagerPrivate.h │ │ │ │ │ ├── THOPluginProtocolPrivate.h │ │ │ │ │ ├── TLOEncryptionManagerPrivate.h │ │ │ │ │ ├── TLOFileLoggerPrivate.h │ │ │ │ │ ├── TLOInputHistoryPrivate.h │ │ │ │ │ ├── TLOLicenseManagerDownloaderPrivate.h │ │ │ │ │ ├── TLOLicenseManagerLastGenPrivate.h │ │ │ │ │ ├── TLOLicenseManagerPrivate.h │ │ │ │ │ ├── TLONicknameCompletionStatusPrivate.h │ │ │ │ │ ├── TLONotificationConfigurationPrivate.h │ │ │ │ │ ├── TLONotificationControllerPrivate.h │ │ │ │ │ ├── TLOSpeechSynthesizerPrivate.h │ │ │ │ │ ├── TLOSpokenNotificationPrivate.h │ │ │ │ │ ├── TPCApplicationInfoPrivate.h │ │ │ │ │ ├── TPCPathInfoPrivate.h │ │ │ │ │ ├── TPCPreferencesImportExportPrivate.h │ │ │ │ │ ├── TPCPreferencesLocalPrivate.h │ │ │ │ │ ├── TPCResourceManagerPrivate.h │ │ │ │ │ ├── TPCSandboxMigrationPrivate.h │ │ │ │ │ ├── TPCThemeControllerPrivate.h │ │ │ │ │ ├── TPCThemePrivate.h │ │ │ │ │ ├── TVCAppearancePrivate.h │ │ │ │ │ ├── TVCChannelSelectionOutlineViewCellPrivate.h │ │ │ │ │ ├── TVCChannelSelectionViewControllerPrivate.h │ │ │ │ │ ├── TVCContentNavigationOutlineViewPrivate.h │ │ │ │ │ ├── TVCDockIconPrivate.h │ │ │ │ │ ├── TVCErrorMessagePopoverControllerPrivate.h │ │ │ │ │ ├── TVCErrorMessagePopoverPrivate.h │ │ │ │ │ ├── TVCLogControllerHistoricLogFilePrivate.h │ │ │ │ │ ├── TVCLogControllerInlineMediaServicePrivate.h │ │ │ │ │ ├── TVCLogControllerOperationQueuePrivate.h │ │ │ │ │ ├── TVCLogControllerPrivate.h │ │ │ │ │ ├── TVCLogLinePrivate.h │ │ │ │ │ ├── TVCLogPolicyPrivate.h │ │ │ │ │ ├── TVCLogScriptEventSinkPrivate.h │ │ │ │ │ ├── TVCLogViewInternalWK1.h │ │ │ │ │ ├── TVCLogViewInternalWK2.h │ │ │ │ │ ├── TVCLogViewPrivate.h │ │ │ │ │ ├── TVCMainWindowAppearancePrivate.h │ │ │ │ │ ├── TVCMainWindowChannelViewPrivate.h │ │ │ │ │ ├── TVCMainWindowLoadingScreenPrivate.h │ │ │ │ │ ├── TVCMainWindowPrivate.h │ │ │ │ │ ├── TVCMainWindowSegmentedControlPrivate.h │ │ │ │ │ ├── TVCMainWindowSplitViewPrivate.h │ │ │ │ │ ├── TVCMainWindowTextViewAppearancePrivate.h │ │ │ │ │ ├── TVCMainWindowTextViewPrivate.h │ │ │ │ │ ├── TVCMainWindowTitlebarAccessoryViewPrivate.h │ │ │ │ │ ├── TVCMemberListAppearancePrivate.h │ │ │ │ │ ├── TVCMemberListCellPrivate.h │ │ │ │ │ ├── TVCMemberListPrivate.h │ │ │ │ │ ├── TVCMemberListUserInfoPopoverPrivate.h │ │ │ │ │ ├── TVCNotificationConfigurationViewControllerPrivate.h │ │ │ │ │ ├── TVCServerListAppearancePrivate.h │ │ │ │ │ ├── TVCServerListCellPrivate.h │ │ │ │ │ ├── TVCServerListPrivate.h │ │ │ │ │ ├── TVCTextFormatterMenuPrivate.h │ │ │ │ │ ├── TVCTextViewWithIRCFormatterPrivate.h │ │ │ │ │ ├── TVCWK1AutoScrollerPrivate.h │ │ │ │ │ ├── TXAppearancePrivate.h │ │ │ │ │ ├── TXApplicationPrivate.h │ │ │ │ │ ├── TXGlobalModelsPrivate.h │ │ │ │ │ ├── TXMasterControllerPrivate.h │ │ │ │ │ ├── TXMenuControllerPrivate.h │ │ │ │ │ ├── TXSharedApplicationPrivate.h │ │ │ │ │ ├── TXWindowControllerPrivate.h │ │ │ │ │ ├── TextualPrivate.h │ │ │ │ │ ├── WKWebViewPrivate.h │ │ │ │ │ └── WebScriptObjectHelperPrivate.h │ │ │ │ ├── TDCAlert.h │ │ │ │ ├── TDCInputPrompt.h │ │ │ │ ├── TDCSheetBase.h │ │ │ │ ├── TDCWindowBase.h │ │ │ │ ├── THOPluginManager.h │ │ │ │ ├── THOPluginProtocol.h │ │ │ │ ├── THOUnicodeHelper.h │ │ │ │ ├── TLOEncryptionManager.h │ │ │ │ ├── TLOInternetAddressLookup.h │ │ │ │ ├── TLOKeyEventHandler.h │ │ │ │ ├── TLOLinkParser.h │ │ │ │ ├── TLONotificationController.h │ │ │ │ ├── TLOSoundPlayer.h │ │ │ │ ├── TLOpenLink.h │ │ │ │ ├── TPCApplicationInfo.h │ │ │ │ ├── TPCPathInfo.h │ │ │ │ ├── TPCPreferencesImportExport.h │ │ │ │ ├── TPCPreferencesLocal.h │ │ │ │ ├── TPCPreferencesReload.h │ │ │ │ ├── TPCPreferencesUserDefaultsLocal.h │ │ │ │ ├── TPCResourceManager.h │ │ │ │ ├── TPCTheme.h │ │ │ │ ├── TPCThemeController.h │ │ │ │ ├── TVCAlert.h │ │ │ │ ├── TVCAppearance.h │ │ │ │ ├── TVCAutoExpandingTextField.h │ │ │ │ ├── TVCAutoExpandingTokenField.h │ │ │ │ ├── TVCBasicTableView.h │ │ │ │ ├── TVCChannelSelectionViewController.h │ │ │ │ ├── TVCLogController.h │ │ │ │ ├── TVCLogLine.h │ │ │ │ ├── TVCLogRenderer.h │ │ │ │ ├── TVCLogView.h │ │ │ │ ├── TVCMainWindow.h │ │ │ │ ├── TVCMainWindowAppearance.h │ │ │ │ ├── TVCMainWindowLoadingScreen.h │ │ │ │ ├── TVCMainWindowSplitView.h │ │ │ │ ├── TVCMainWindowTextView.h │ │ │ │ ├── TVCMainWindowTextViewAppearance.h │ │ │ │ ├── TVCMemberList.h │ │ │ │ ├── TVCMemberListAppearance.h │ │ │ │ ├── TVCServerList.h │ │ │ │ ├── TVCServerListAppearance.h │ │ │ │ ├── TVCTextViewWithIRCFormatter.h │ │ │ │ ├── TVCValidatedComboBox.h │ │ │ │ ├── TVCValidatedTextField.h │ │ │ │ ├── TXAppearance.h │ │ │ │ ├── TXAppearanceHelper.h │ │ │ │ ├── TXGlobalModels.h │ │ │ │ ├── TXMasterController.h │ │ │ │ ├── TXMenuController.h │ │ │ │ ├── TXSharedApplication.h │ │ │ │ ├── Textual.h │ │ │ │ └── TextualApplication.h │ │ │ ├── Helpers/ │ │ │ │ ├── Cocoa (Objective-C)/ │ │ │ │ │ ├── NSColorHelper.m │ │ │ │ │ ├── NSStringHelper.m │ │ │ │ │ ├── NSTableViewHelper.m │ │ │ │ │ ├── NSViewHelper.m │ │ │ │ │ └── TXAppearanceHelper.m │ │ │ │ ├── External Libraries/ │ │ │ │ │ ├── Google/ │ │ │ │ │ │ └── GTMEncodeHTML.m │ │ │ │ │ └── WebKit/ │ │ │ │ │ └── WebScriptObjectHelper.m │ │ │ │ ├── Plugin Architecture/ │ │ │ │ │ ├── THOPluginDispatcher.m │ │ │ │ │ ├── THOPluginItem.m │ │ │ │ │ ├── THOPluginItemLogging.m │ │ │ │ │ └── THOPluginManager.m │ │ │ │ └── THOUnicodeHelper.m │ │ │ ├── IRC/ │ │ │ │ ├── IRCAddressBook.m │ │ │ │ ├── IRCAddressBookMatchCache.m │ │ │ │ ├── IRCAddressBookUserTracking.m │ │ │ │ ├── IRCChannel.m │ │ │ │ ├── IRCChannelConfig.m │ │ │ │ ├── IRCChannelMemberList.m │ │ │ │ ├── IRCChannelMemberListController.m │ │ │ │ ├── IRCChannelMode.m │ │ │ │ ├── IRCClient.m │ │ │ │ ├── IRCClientConfig.m │ │ │ │ ├── IRCClientRequestedCommands.m │ │ │ │ ├── IRCCommandIndex.m │ │ │ │ ├── IRCConnection.m │ │ │ │ ├── IRCExtras.m │ │ │ │ ├── IRCHighlightLogEntry.m │ │ │ │ ├── IRCHighlightMatchCondition.m │ │ │ │ ├── IRCISupportInfo.m │ │ │ │ ├── IRCMessage.m │ │ │ │ ├── IRCMessageBatch.m │ │ │ │ ├── IRCModeInfo.m │ │ │ │ ├── IRCNetworkList.m │ │ │ │ ├── IRCPrefix.m │ │ │ │ ├── IRCSendingMessage.m │ │ │ │ ├── IRCServer.m │ │ │ │ ├── IRCTimerCommand.m │ │ │ │ ├── IRCTreeItem.m │ │ │ │ ├── IRCWorld.m │ │ │ │ └── Users/ │ │ │ │ ├── IRCChannelUser.m │ │ │ │ ├── IRCUser.m │ │ │ │ ├── IRCUserNicknameColorStyleGenerator.m │ │ │ │ ├── IRCUserPersistentStore.m │ │ │ │ └── IRCUserRelations.m │ │ │ ├── Library/ │ │ │ │ ├── Color Formatting/ │ │ │ │ │ └── IRCColorFormat.m │ │ │ │ ├── External Libraries/ │ │ │ │ │ └── Sockets/ │ │ │ │ │ └── OELReachability.m │ │ │ │ ├── License Manager/ │ │ │ │ │ └── Standalone/ │ │ │ │ │ ├── TLOLicenseManager.m │ │ │ │ │ ├── TLOLicenseManagerDownloader.m │ │ │ │ │ └── TLOLicenseManagerLastGen.m │ │ │ │ ├── TLOEncryptionManager.m │ │ │ │ ├── TLOFileLogger.m │ │ │ │ ├── TLOInputHistory.m │ │ │ │ ├── TLOInternetAddressLookup.m │ │ │ │ ├── TLOKeyEventHandler.m │ │ │ │ ├── TLOLinkParser.swift │ │ │ │ ├── TLONicknameCompletionStatus.m │ │ │ │ ├── TLONotificationConfiguration.m │ │ │ │ ├── TLONotificationController.m │ │ │ │ ├── TLOSoundPlayer.m │ │ │ │ ├── TLOSpeechSynthesizer.m │ │ │ │ ├── TLOSpokenNotification.m │ │ │ │ └── TLOpenLink.swift │ │ │ ├── Others/ │ │ │ │ └── main.m │ │ │ ├── Preferences/ │ │ │ │ ├── TPCApplicationInfo.m │ │ │ │ ├── TPCPathInfo.m │ │ │ │ ├── TPCPreferencesImportExport.m │ │ │ │ ├── TPCPreferencesLocal.m │ │ │ │ ├── TPCPreferencesReload.m │ │ │ │ ├── TPCPreferencesUserDefaultsLocal.m │ │ │ │ ├── TPCResourceManager.m │ │ │ │ ├── TPCSandboxMigration.m │ │ │ │ └── Themes/ │ │ │ │ ├── TPCTheme.m │ │ │ │ └── TPCThemeController.m │ │ │ ├── Services/ │ │ │ │ └── ICLPayloadLocal.m │ │ │ └── Views/ │ │ │ ├── Channel Selection Table/ │ │ │ │ ├── TVCChannelSelectionOutlineCellView.m │ │ │ │ └── TVCChannelSelectionViewController.m │ │ │ ├── Channel View/ │ │ │ │ ├── Extras/ │ │ │ │ │ ├── TVCLogControllerHistoricLogFile.m │ │ │ │ │ ├── TVCLogControllerInlineMediaService.m │ │ │ │ │ └── TVCLogControllerOperationQueue.m │ │ │ │ ├── TVCLogController.m │ │ │ │ ├── TVCLogLine.m │ │ │ │ ├── TVCLogPolicy.m │ │ │ │ ├── TVCLogRenderer.m │ │ │ │ ├── TVCLogScriptEventSink.m │ │ │ │ ├── TVCLogView.m │ │ │ │ ├── TVCLogViewInternalWK1.m │ │ │ │ ├── TVCLogViewInternalWK2.m │ │ │ │ └── TVCWK1AutoScroller.m │ │ │ ├── Errors/ │ │ │ │ ├── TVCAlert.m │ │ │ │ ├── TVCErrorMessagePopover.m │ │ │ │ └── TVCErrorMessagePopoverController.m │ │ │ ├── Input Text Field/ │ │ │ │ ├── TVCAutoExpandingTextField.m │ │ │ │ ├── TVCAutoExpandingTokenField.m │ │ │ │ ├── TVCTextFormatterMenu.m │ │ │ │ ├── TVCTextViewWithIRCFormatter.m │ │ │ │ ├── TVCValidatedComboBox.m │ │ │ │ └── TVCValidatedTextField.m │ │ │ ├── Main Window/ │ │ │ │ ├── TVCMainWindow.m │ │ │ │ ├── TVCMainWindowAppearance.m │ │ │ │ ├── TVCMainWindowChannelView.m │ │ │ │ ├── TVCMainWindowLoadingScreen.m │ │ │ │ ├── TVCMainWindowSegmentedControl.m │ │ │ │ ├── TVCMainWindowSplitView.m │ │ │ │ ├── TVCMainWindowTextView.m │ │ │ │ ├── TVCMainWindowTextViewAppearance.m │ │ │ │ └── TVCMainWindowTitlebarAccessoryView.m │ │ │ ├── Preferences/ │ │ │ │ └── TVCNotificationConfigurationViewController.m │ │ │ ├── Server List/ │ │ │ │ ├── TVCServerList.m │ │ │ │ ├── TVCServerListAppearance.m │ │ │ │ └── TVCServerListCell.m │ │ │ ├── TVCAppearance.m │ │ │ ├── TVCBasicTableView.m │ │ │ ├── TVCContentNavigationOutlineView.m │ │ │ ├── TVCDockIcon.m │ │ │ └── User List/ │ │ │ ├── TVCMemberList.m │ │ │ ├── TVCMemberListAppearance.m │ │ │ ├── TVCMemberListCell.m │ │ │ └── TVCMemberListUserInfoPopover.m │ │ ├── Configurations/ │ │ │ └── Sandbox/ │ │ │ ├── Debug.entitlements │ │ │ ├── Sandbox Disabled.entitlements │ │ │ └── Standard Release.entitlements │ │ ├── Resources/ │ │ │ ├── Images/ │ │ │ │ ├── Application/ │ │ │ │ │ ├── applicationIcon.icns │ │ │ │ │ ├── applicationIcon.psd │ │ │ │ │ └── applicationIconBirthday.icns │ │ │ │ ├── Copyright Information for Images.txt │ │ │ │ ├── Dock Icon Badges/ │ │ │ │ │ └── Photoshop/ │ │ │ │ │ ├── Mavericks/ │ │ │ │ │ │ ├── blueDockBadge.psd │ │ │ │ │ │ ├── blueDockBadgeWithShadow.psd │ │ │ │ │ │ ├── redDockBadge.psd │ │ │ │ │ │ └── redDockBadgeWithShadow.psd │ │ │ │ │ └── Yosemite/ │ │ │ │ │ └── redDockBadge.psd │ │ │ │ ├── Encryption Badges/ │ │ │ │ │ ├── encryptionLockIconDark.tif │ │ │ │ │ ├── encryptionLockIconDark@2x.tif │ │ │ │ │ ├── encryptionLockIconLight.tif │ │ │ │ │ └── encryptionLockIconLight@2x.tif │ │ │ │ ├── IRC Formatting Colors/ │ │ │ │ │ ├── FormattingColor_Rainbow.tif │ │ │ │ │ └── FormattingColor_Rainbow@2x.tif │ │ │ │ ├── Status Badges/ │ │ │ │ │ ├── channelRoomStatusIconDarkActive.tif │ │ │ │ │ ├── channelRoomStatusIconDarkActive@2x.tif │ │ │ │ │ ├── channelRoomStatusIconDarkInactive.tif │ │ │ │ │ ├── channelRoomStatusIconDarkInactive@2x.tif │ │ │ │ │ ├── channelRoomStatusIconLightActive.tif │ │ │ │ │ ├── channelRoomStatusIconLightActive@2x.tif │ │ │ │ │ ├── channelRoomStatusIconLightInactive.tif │ │ │ │ │ ├── channelRoomStatusIconLightInactive@2x.tif │ │ │ │ │ ├── channelRoomStatusIconMavericksDark.psd │ │ │ │ │ └── channelRoomStatusIconMavericksLight.psd │ │ │ │ └── User Interface/ │ │ │ │ ├── Miscellaneous/ │ │ │ │ │ └── ErroneousTextFieldValueIndicator.tif │ │ │ │ └── Server List/ │ │ │ │ └── Glass/ │ │ │ │ ├── VibrantDarkServerListViewPrivateMessageUserIconActive.tif │ │ │ │ ├── VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif │ │ │ │ ├── VibrantDarkServerListViewPrivateMessageUserIconInactive.tif │ │ │ │ ├── VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif │ │ │ │ ├── VibrantLightServerListViewPrivateMessageUserIconActive.tif │ │ │ │ ├── VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif │ │ │ │ ├── VibrantLightServerListViewPrivateMessageUserIconInactive.tif │ │ │ │ └── VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif │ │ │ ├── Language Files/ │ │ │ │ ├── _randomToken │ │ │ │ └── en.lproj/ │ │ │ │ ├── Accessibility.strings │ │ │ │ ├── BasicLanguage.strings │ │ │ │ ├── CommonErrors.strings │ │ │ │ ├── IRC.strings │ │ │ │ ├── Notifications.strings │ │ │ │ ├── OffTheRecord.strings │ │ │ │ ├── Prompts.strings │ │ │ │ ├── TDCAboutDialog.strings │ │ │ │ ├── TDCAddressBookSheet.strings │ │ │ │ ├── TDCChannelBanListSheet.strings │ │ │ │ ├── TDCChannelInviteSheet.strings │ │ │ │ ├── TDCChannelModifyModesSheet.strings │ │ │ │ ├── TDCChannelModifyTopicSheet.strings │ │ │ │ ├── TDCChannelPropertiesSheet.strings │ │ │ │ ├── TDCChannelSpotlightController.strings │ │ │ │ ├── TDCFileTransferDialog.strings │ │ │ │ ├── TDCLicenseUpgradeEligibilitySheet.strings │ │ │ │ ├── TDCPreferencesController.strings │ │ │ │ ├── TDCPreferencesUserStyleSheet.strings │ │ │ │ ├── TDCServerChannelListDialog.strings │ │ │ │ ├── TDCServerEndpointListSheet.strings │ │ │ │ ├── TDCServerPropertiesSheet.strings │ │ │ │ ├── TLOLicenseManager.strings │ │ │ │ ├── TVCMainWindow.strings │ │ │ │ └── TVCNotificationConfigurationView.strings │ │ │ ├── License Manager/ │ │ │ │ └── RemoteLicenseSystemPublicKey.pub │ │ │ ├── Property Lists/ │ │ │ │ ├── Application Properties/ │ │ │ │ │ └── Info.plist │ │ │ │ ├── IRCCommandIndexLocalData.plist │ │ │ │ ├── IRCCommandIndexRemoteData.plist │ │ │ │ ├── IRCNetworks.plist │ │ │ │ ├── Preferences/ │ │ │ │ │ ├── KeysExcludedFromContainer.plist │ │ │ │ │ ├── KeysExcludedFromExport.plist │ │ │ │ │ ├── KeysExcludedFromMigrate.plist │ │ │ │ │ ├── PreferenceKeyMasterList.plist │ │ │ │ │ ├── RegisteredUserDefaults.plist │ │ │ │ │ └── RegisteredUserDefaultsInContainer.plist │ │ │ │ ├── StaticStore.plist │ │ │ │ └── TemplateLineTypes.plist │ │ │ ├── Scripting/ │ │ │ │ └── Script Files/ │ │ │ │ ├── Bundled Scripts/ │ │ │ │ │ ├── Installing Custom Scripts.txt │ │ │ │ │ ├── date.scpt │ │ │ │ │ └── moti.scpt │ │ │ │ └── Textual Extras Installer/ │ │ │ │ ├── Create-Installers │ │ │ │ ├── Installation Files/ │ │ │ │ │ └── Scripts/ │ │ │ │ │ ├── apps.scpt │ │ │ │ │ ├── banhammer.scpt │ │ │ │ │ ├── ffuu.scpt │ │ │ │ │ ├── flip.scpt │ │ │ │ │ ├── hermes.scpt │ │ │ │ │ ├── music.scpt │ │ │ │ │ ├── np.scpt │ │ │ │ │ ├── o_o.scpt │ │ │ │ │ ├── page.scpt │ │ │ │ │ ├── qt.scpt │ │ │ │ │ ├── radium.scpt │ │ │ │ │ ├── reverse.scpt │ │ │ │ │ ├── shell.scpt │ │ │ │ │ ├── slap.scpt │ │ │ │ │ ├── spotify.scpt │ │ │ │ │ └── vlc.scpt │ │ │ │ ├── Installer Helpers/ │ │ │ │ │ ├── Build-Extension-Package │ │ │ │ │ ├── Build-Final-Package │ │ │ │ │ └── Build-Scripts-Package │ │ │ │ ├── Installer Resources/ │ │ │ │ │ └── en.lproj/ │ │ │ │ │ ├── Localizable.strings │ │ │ │ │ └── Welcome.rtf │ │ │ │ ├── Packages/ │ │ │ │ │ └── Textual-Extras.pkg │ │ │ │ └── distribution.plist │ │ │ ├── Styling/ │ │ │ │ ├── Bundled Styles/ │ │ │ │ │ ├── Astria/ │ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ │ └── encryptedMessageLock.mustache │ │ │ │ │ │ ├── copyright.txt │ │ │ │ │ │ ├── design.css │ │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ │ ├── scripts.js │ │ │ │ │ │ └── settings.plist │ │ │ │ │ ├── Equinox/ │ │ │ │ │ │ ├── LICENSE.txt │ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ │ └── encryptedMessageLock.mustache │ │ │ │ │ │ ├── design.css │ │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ │ ├── scripts.js │ │ │ │ │ │ └── settings.plist │ │ │ │ │ ├── Sapientia/ │ │ │ │ │ │ ├── Resources/ │ │ │ │ │ │ │ └── Documentation/ │ │ │ │ │ │ │ └── changelog.txt │ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ │ └── encryptedMessageLock.mustache │ │ │ │ │ │ ├── copyright.txt │ │ │ │ │ │ ├── design.css │ │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ │ ├── scripts.js │ │ │ │ │ │ └── settings.plist │ │ │ │ │ ├── Simplified/ │ │ │ │ │ │ ├── Varieties/ │ │ │ │ │ │ │ ├── Dark/ │ │ │ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ │ │ │ └── encryptedMessageLock.mustache │ │ │ │ │ │ │ │ ├── design.css │ │ │ │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ │ │ │ └── settings.plist │ │ │ │ │ │ │ └── Light/ │ │ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ │ │ └── encryptedMessageLock.mustache │ │ │ │ │ │ │ ├── design.css │ │ │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ │ │ └── settings.plist │ │ │ │ │ │ ├── copyright.txt │ │ │ │ │ │ ├── scripts.js │ │ │ │ │ │ └── settings.plist │ │ │ │ │ ├── Sulaco/ │ │ │ │ │ │ ├── LICENSE.txt │ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ │ ├── newMessagePostedWithSender.mustache │ │ │ │ │ │ │ └── newMessagePostedWithoutSender.mustache │ │ │ │ │ │ ├── design.css │ │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ │ ├── scripts.js │ │ │ │ │ │ └── settings.plist │ │ │ │ │ ├── Tomorrow/ │ │ │ │ │ │ ├── Varieties/ │ │ │ │ │ │ │ ├── Dark/ │ │ │ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ │ │ │ └── encryptedMessageLock.mustache │ │ │ │ │ │ │ │ ├── design.css │ │ │ │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ │ │ │ └── settings.plist │ │ │ │ │ │ │ └── Light/ │ │ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ │ │ └── encryptedMessageLock.mustache │ │ │ │ │ │ │ ├── design.css │ │ │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ │ │ └── settings.plist │ │ │ │ │ │ ├── copyright.txt │ │ │ │ │ │ ├── scripts.js │ │ │ │ │ │ └── settings.plist │ │ │ │ │ └── Total Sublime/ │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ └── encryptedMessageLock.mustache │ │ │ │ │ ├── copyright.txt │ │ │ │ │ ├── design.css │ │ │ │ │ ├── inlineMedia.css │ │ │ │ │ ├── scripts.js │ │ │ │ │ └── settings.plist │ │ │ │ ├── JavaScript/ │ │ │ │ │ └── API/ │ │ │ │ │ ├── core.js │ │ │ │ │ ├── corePrivate.js │ │ │ │ │ └── private/ │ │ │ │ │ ├── conversationTracking.js │ │ │ │ │ ├── core/ │ │ │ │ │ │ ├── clickMenuSelection.js │ │ │ │ │ │ ├── documentBody.js │ │ │ │ │ │ ├── events.js │ │ │ │ │ │ ├── inlineMedia.js │ │ │ │ │ │ ├── messageBuffer.js │ │ │ │ │ │ └── scrollTo.js │ │ │ │ │ ├── scriptSink.js │ │ │ │ │ └── scroller/ │ │ │ │ │ ├── automatic.js │ │ │ │ │ ├── automaticEmpty.js │ │ │ │ │ └── state.js │ │ │ │ └── Style Default Templates/ │ │ │ │ └── Version 4/ │ │ │ │ ├── baseLayout.css │ │ │ │ ├── baseLayout.mustache │ │ │ │ ├── dateIndicator.mustache │ │ │ │ ├── encryptedMessageLock.mustache │ │ │ │ ├── formattedMessageFragment.mustache │ │ │ │ ├── historyIndicator.mustache │ │ │ │ ├── messageBufferLoadingIndicator.mustache │ │ │ │ ├── messageBufferSessionIndicator.mustache │ │ │ │ ├── newMessagePostedWithSender.mustache │ │ │ │ ├── newMessagePostedWithoutSender.mustache │ │ │ │ ├── renderedChannelNameLinkResource.mustache │ │ │ │ ├── renderedStandardAnchorLinkResource.mustache │ │ │ │ └── sessionIndicator.mustache │ │ │ └── User Interface/ │ │ │ ├── Appearance/ │ │ │ │ ├── TDCChannelSpotlightAppearance.plist │ │ │ │ ├── TDCChannelSpotlightAppearanceTemplate.plist │ │ │ │ ├── TVCMainWindowAppearance.plist │ │ │ │ ├── TVCMainWindowAppearanceTemplate.plist │ │ │ │ ├── TVCMainWindowTextViewAppearance.plist │ │ │ │ ├── TVCMainWindowTextViewAppearanceTemplate.plist │ │ │ │ ├── TVCMemberListAppearance.plist │ │ │ │ ├── TVCMemberListAppearanceTemplate.plist │ │ │ │ ├── TVCServerListAppearance.plist │ │ │ │ └── TVCServerListAppearanceTemplate.plist │ │ │ └── en.lproj/ │ │ │ ├── TDCAboutDialog.xib │ │ │ ├── TDCAddressBookSheet.xib │ │ │ ├── TDCChannelBanListSheet.xib │ │ │ ├── TDCChannelInviteSheet.xib │ │ │ ├── TDCChannelModifyModesSheet.xib │ │ │ ├── TDCChannelModifyTopicSheet.xib │ │ │ ├── TDCChannelPropertiesSheet.xib │ │ │ ├── TDCChannelSpotlightController.xib │ │ │ ├── TDCFileTransferDialog.xib │ │ │ ├── TDCHighlightEntrySheet.xib │ │ │ ├── TDCLicenseManagerDialog.xib │ │ │ ├── TDCLicenseManagerMigrateAppStoreSheet.xib │ │ │ ├── TDCLicenseManagerRecoverLostLicenseSheet.xib │ │ │ ├── TDCLicenseUpgradeActivateSheet.xib │ │ │ ├── TDCLicenseUpgradeDialog.xib │ │ │ ├── TDCLicenseUpgradeEligibilitySheet.xib │ │ │ ├── TDCNicknameColorSheet.xib │ │ │ ├── TDCPreferences.xib │ │ │ ├── TDCPreferencesUserStyleSheet.xib │ │ │ ├── TDCProgressIndicatorSheet.xib │ │ │ ├── TDCServerChangeNicknameSheet.xib │ │ │ ├── TDCServerChannelListDialog.xib │ │ │ ├── TDCServerEndpointListSheet.xib │ │ │ ├── TDCServerHighlightListSheet.xib │ │ │ ├── TDCServerPropertiesSheet.xib │ │ │ ├── TDCWelcomeSheet.xib │ │ │ ├── TVCAlert.xib │ │ │ ├── TVCChannelSelectionView.xib │ │ │ ├── TVCMainWindow.xib │ │ │ ├── TVCNotificationConfigurationView.xib │ │ │ └── TXCMainMenu.xib │ │ └── Textual App.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ ├── Textual (App Store).xcscheme │ │ ├── Textual (Debug).xcscheme │ │ ├── Textual (Standard Release Sandboxed).xcscheme │ │ └── Textual (Standard Release).xcscheme │ ├── Plugins/ │ │ ├── Blowfish Encryption/ │ │ │ ├── ACKNOWLEDGEMENT.txt │ │ │ ├── Blowfish Encryption Extension.xcodeproj/ │ │ │ │ ├── project.pbxproj │ │ │ │ └── xcshareddata/ │ │ │ │ └── xcschemes/ │ │ │ │ └── Blowfish Encryption Extension.xcscheme │ │ │ ├── Classes/ │ │ │ │ ├── BlowfishEncryption.h │ │ │ │ ├── BlowfishEncryption.m │ │ │ │ ├── BlowfishEncryptionBase.h │ │ │ │ ├── BlowfishEncryptionBase.m │ │ │ │ ├── BlowfishEncryptionKeyExchange.h │ │ │ │ ├── BlowfishEncryptionKeyExchange.mm │ │ │ │ ├── BlowfishEncryptionKeyExchangeBase.h │ │ │ │ ├── BlowfishEncryptionKeyExchangeBase.mm │ │ │ │ ├── NSDataHelper.h │ │ │ │ ├── NSDataHelper.m │ │ │ │ ├── TPIBlowfishEncryption.h │ │ │ │ ├── TPIBlowfishEncryption.m │ │ │ │ ├── TPIBlowfishEncryptionSwizzledClasses.h │ │ │ │ └── TPIBlowfishEncryptionSwizzledClasses.m │ │ │ ├── LICENSE-GPLv2.txt │ │ │ ├── LICENSE.txt │ │ │ └── Resources/ │ │ │ ├── Language Files/ │ │ │ │ └── en.lproj/ │ │ │ │ └── BasicLanguage.strings │ │ │ ├── Property Lists/ │ │ │ │ └── Info.plist │ │ │ └── User Interface/ │ │ │ └── en.lproj/ │ │ │ └── TPIBlowfishEncryption.xib │ │ ├── Caffeine/ │ │ │ ├── Caffeine Extension.xcodeproj/ │ │ │ │ ├── project.pbxproj │ │ │ │ └── xcshareddata/ │ │ │ │ └── xcschemes/ │ │ │ │ └── Caffeine Extension.xcscheme │ │ │ ├── Classes/ │ │ │ │ ├── TPI_Caffeine.h │ │ │ │ └── TPI_Caffeine.m │ │ │ └── Resources/ │ │ │ ├── Language Files/ │ │ │ │ └── en.lproj/ │ │ │ │ └── BasicLanguage.strings │ │ │ ├── Property Lists/ │ │ │ │ └── Info.plist │ │ │ └── User Interface/ │ │ │ └── en.lproj/ │ │ │ └── TPI_Caffeine.xib │ │ ├── Chat Filter/ │ │ │ ├── Chat Filter Extension.xcodeproj/ │ │ │ │ └── project.pbxproj │ │ │ ├── Classes/ │ │ │ │ ├── TPI_ChatFilter.h │ │ │ │ ├── TPI_ChatFilter.m │ │ │ │ ├── TPI_ChatFilterEditFilterSheet.h │ │ │ │ ├── TPI_ChatFilterEditFilterSheet.m │ │ │ │ ├── TPI_ChatFilterExtension.h │ │ │ │ ├── TPI_ChatFilterExtension.m │ │ │ │ ├── TPI_ChatFilterInternal.h │ │ │ │ ├── TPI_ChatFilterLogic.h │ │ │ │ ├── TPI_ChatFilterLogic.m │ │ │ │ ├── TPI_NumberOnlyTextFieldFormatter.h │ │ │ │ └── TPI_NumberOnlyTextFieldFormatter.m │ │ │ └── Resources/ │ │ │ ├── Language Files/ │ │ │ │ └── en.lproj/ │ │ │ │ ├── TPI_ChatFilterEditFilterSheet.strings │ │ │ │ ├── TPI_ChatFilterExtension.strings │ │ │ │ └── TPI_ChatFilterLogic.strings │ │ │ ├── Property Lists/ │ │ │ │ └── Info.plist │ │ │ └── User Interface/ │ │ │ └── en.lproj/ │ │ │ ├── TPI_ChatFilterEditFilterSheet.xib │ │ │ └── TPI_ChatFilterExtension.xib │ │ ├── Sample Code/ │ │ │ └── Preference Pane/ │ │ │ ├── Classes/ │ │ │ │ ├── TPI_PreferencePaneExample.h │ │ │ │ └── TPI_PreferencePaneExample.m │ │ │ ├── PreferencePaneExample.xcodeproj/ │ │ │ │ └── project.pbxproj │ │ │ └── Resources/ │ │ │ ├── Property Lists/ │ │ │ │ └── Info.plist │ │ │ └── User Interface/ │ │ │ └── PreferencePane.xib │ │ ├── Smiley Converter/ │ │ │ ├── Classes/ │ │ │ │ ├── TPISmileyConverter.h │ │ │ │ └── TPISmileyConverter.m │ │ │ ├── Resources/ │ │ │ │ ├── Language Files/ │ │ │ │ │ └── en.lproj/ │ │ │ │ │ └── BasicLanguage.strings │ │ │ │ ├── Property Lists/ │ │ │ │ │ ├── Info.plist │ │ │ │ │ ├── conversionTable.plist │ │ │ │ │ └── conversionTable2.plist │ │ │ │ └── User Interface/ │ │ │ │ └── en.lproj/ │ │ │ │ └── TPISmileyConverter.xib │ │ │ └── Smiley Converter Extension.xcodeproj/ │ │ │ └── project.pbxproj │ │ ├── System Profiler/ │ │ │ ├── Classes/ │ │ │ │ ├── SystemProfiler.h │ │ │ │ ├── TPISystemProfiler.h │ │ │ │ ├── TPISystemProfiler.m │ │ │ │ ├── TPI_SP_SysInfo.h │ │ │ │ └── TPI_SP_SysInfo.m │ │ │ ├── Resources/ │ │ │ │ ├── Language Files/ │ │ │ │ │ └── en.lproj/ │ │ │ │ │ └── BasicLanguage.strings │ │ │ │ ├── Property Lists/ │ │ │ │ │ ├── Info.plist │ │ │ │ │ └── MacintoshModels.plist │ │ │ │ └── User Interface/ │ │ │ │ └── en.lproj/ │ │ │ │ └── TPISystemProfiler.xib │ │ │ └── System Profiler Extension.xcodeproj/ │ │ │ └── project.pbxproj │ │ ├── User Insights/ │ │ │ ├── Classes/ │ │ │ │ ├── TPIUserInsights.h │ │ │ │ └── TPIUserInsights.m │ │ │ ├── Resources/ │ │ │ │ ├── Language Files/ │ │ │ │ │ └── en.lproj/ │ │ │ │ │ └── BasicLanguage.strings │ │ │ │ └── Property Lists/ │ │ │ │ └── Info.plist │ │ │ └── User Insights Extension.xcodeproj/ │ │ │ └── project.pbxproj │ │ ├── Wiki Link Parser/ │ │ │ ├── Classes/ │ │ │ │ ├── TPIWikiStyleLinkParser.h │ │ │ │ └── TPIWikiStyleLinkParser.m │ │ │ ├── Resources/ │ │ │ │ ├── Language Files/ │ │ │ │ │ └── en.lproj/ │ │ │ │ │ └── BasicLanguage.strings │ │ │ │ ├── Property Lists/ │ │ │ │ │ └── Info.plist │ │ │ │ └── User Interface/ │ │ │ │ └── en.lproj/ │ │ │ │ └── TPIWikiStyleLinkParser.xib │ │ │ └── Wiki-style Link Parser Extension.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Wiki-style Link Parser Extension.xcscheme │ │ └── ZNC Additions/ │ │ ├── Classes/ │ │ │ ├── TPI_ZNCAdditions.h │ │ │ └── TPI_ZNCAdditions.m │ │ ├── Resources/ │ │ │ ├── Language Files/ │ │ │ │ └── en.lproj/ │ │ │ │ └── BasicLanguage.strings │ │ │ └── Property Lists/ │ │ │ └── Info.plist │ │ └── ZNC Additions Extension.xcodeproj/ │ │ └── project.pbxproj │ └── Shared/ │ ├── Headers/ │ │ ├── External Libraries/ │ │ │ ├── GCDAsyncSocket.h │ │ │ └── GCDAsyncSocketExtensions.h │ │ ├── IRCConnectionConfig.h │ │ ├── IRCConnectionErrors.h │ │ ├── Internal/ │ │ │ └── IRCConnectionConfigInternal.h │ │ ├── Private/ │ │ │ ├── NSObjectHelperPrivate.h │ │ │ ├── TPCPreferencesPrivate.h │ │ │ ├── TPCPreferencesUserDefaultsPrivate.h │ │ │ └── TVCLogLineXPCPrivate.h │ │ ├── StaticDefinitions.h │ │ ├── TLOLocalization.h │ │ ├── TLOTimer.h │ │ ├── TPCPreferences.h │ │ └── TPCPreferencesUserDefaults.h │ ├── Helpers/ │ │ └── Cocoa (Objective-C)/ │ │ └── NSObjectHelper.m │ ├── IRC/ │ │ ├── IRCConnectionConfig.m │ │ └── IRCConnectionErrors.m │ ├── Library/ │ │ ├── External Libraries/ │ │ │ └── Sockets/ │ │ │ ├── GCDAsyncSocket-patch.txt │ │ │ ├── GCDAsyncSocket.m │ │ │ └── GCDAsyncSocketExtensions.m │ │ ├── TLOLocalization.m │ │ ├── TLOLocalization.swift │ │ └── TLOTimer.m │ ├── Preferences/ │ │ ├── TPCPreferences.m │ │ └── TPCPreferencesUserDefaults.m │ └── Views/ │ └── Channel View/ │ └── TVCLogLineXPC.m └── XPC Services/ ├── Historic Log File Manager/ │ ├── Classes/ │ │ ├── HLSHistoricLogLineEntityMigration.m │ │ ├── HLSHistoricLogProcessMain.m │ │ ├── HLSHistoricLogViewContext.m │ │ ├── HSLHistoricLogProcessDelegate.m │ │ ├── Headers/ │ │ │ └── Private/ │ │ │ ├── HLSHistoricLogLineEntityMigrationPrivate.h │ │ │ ├── HLSHistoricLogProcessMainPrivate.h │ │ │ ├── HLSHistoricLogProtocol.h │ │ │ ├── HLSHistoricLogViewContextPrivate.h │ │ │ ├── HSLHistoricLogPCHPrivate.h │ │ │ └── HSLHistoricLogProcessDelegatePrivate.h │ │ └── main.m │ ├── Configurations/ │ │ └── Sandbox/ │ │ └── Release.entitlements │ ├── Historic Log File Manager.xcodeproj/ │ │ └── project.pbxproj │ └── Resources/ │ ├── Data Models/ │ │ ├── HistoricLogFileStorageModel.xcdatamodeld/ │ │ │ ├── .xccurrentversion │ │ │ ├── LogControllerStorageModel (model 2).xcdatamodel/ │ │ │ │ └── contents │ │ │ └── LogControllerStorageModel (model 3).xcdatamodel/ │ │ │ └── contents │ │ └── HistoricLogFileStorageModel.xcmappingmodel/ │ │ └── xcmapping.xml │ └── Property Lists/ │ └── Info.plist ├── IRC Remote Connection Manager/ │ ├── Classes/ │ │ ├── Headers/ │ │ │ └── Private/ │ │ │ ├── IRCConnectionPrivate.h │ │ │ ├── RCMConnectionManagerProtocol.h │ │ │ ├── RCMProcessDelegatePrivate.h │ │ │ ├── RCMProcessMainPrivate.h │ │ │ ├── RCMProcessPCHPrivate.h │ │ │ └── SwiftBridgingHeaderPrivate.h │ │ ├── IRC/ │ │ │ ├── IRCConnection.swift │ │ │ ├── IRCConnectionSocket.swift │ │ │ ├── IRCConnectionSocketClassic.swift │ │ │ └── IRCConnectionSocketNWF.swift │ │ └── Service/ │ │ ├── RCMProcessDelegate.m │ │ ├── RCMProcessMain.m │ │ └── main.m │ ├── Configurations/ │ │ └── Sandbox/ │ │ └── Release.entitlements │ ├── IRC Remote Connection Manager.xcodeproj/ │ │ └── project.pbxproj │ └── Resources/ │ ├── Language Files/ │ │ └── en.lproj/ │ │ └── ConnectionErrors.strings │ └── Property Lists/ │ └── Info.plist └── Inline Content Loader/ ├── Build Scripts/ │ └── BuildExtensions.sh ├── Classes/ │ ├── Headers/ │ │ ├── ICLHelpers.h │ │ ├── ICLInlineContentModule.h │ │ ├── ICLMediaAssessment.h │ │ ├── ICLMediaAssessor.h │ │ ├── ICLMediaType.h │ │ ├── ICLPayload.h │ │ ├── ICLPluginProtocol.h │ │ ├── Internal/ │ │ │ ├── ICLInlineContentModuleInternal.h │ │ │ ├── ICLMediaAssessmentInternal.h │ │ │ └── ICLPayloadInternal.h │ │ └── Private/ │ │ ├── CoreModulesImportsPrivate.h │ │ ├── ICLInlineContentModulePrivate.h │ │ ├── ICLInlineContentProtocol.h │ │ ├── ICLPayloadPrivate.h │ │ ├── ICLPluginManagerPrivate.h │ │ ├── ICLProcessDelegatePrivate.h │ │ ├── ICLProcessMainPrivate.h │ │ └── ICLProcessPCHPrivate.h │ ├── Modules/ │ │ ├── Assessed Media/ │ │ │ ├── ICMAssessedMedia.h │ │ │ └── ICMAssessedMedia.m │ │ └── Root Classes/ │ │ ├── ICMInlineHTML.h │ │ ├── ICMInlineHTML.m │ │ ├── ICMInlineImage.h │ │ ├── ICMInlineImage.m │ │ ├── ICMInlineVideo.h │ │ └── ICMInlineVideo.m │ └── Service/ │ ├── ICLHelpers.m │ ├── ICLInlineContentModule.m │ ├── ICLMediaAssessment.m │ ├── ICLMediaAssessor.m │ ├── ICLPayloadLocal.m │ ├── ICLPayloadShared.m │ ├── ICLPluginManager.m │ ├── ICLProcessDelegate.m │ ├── ICLProcessMain.m │ └── main.m ├── Configurations/ │ └── Sandbox/ │ └── Release.entitlements ├── Extensions/ │ └── Core Media/ │ ├── Classes/ │ │ ├── Headers/ │ │ │ └── Private/ │ │ │ ├── ICPCoreMediaPCHPrivate.h │ │ │ └── ICPCoreMediaPrivate.h │ │ ├── ICPCoreMedia.m │ │ └── Modules/ │ │ ├── Common Images/ │ │ │ ├── ICMCommonInlineImages.h │ │ │ └── ICMCommonInlineImages.m │ │ ├── Common Videos/ │ │ │ ├── ICMCommonInlineVideos.h │ │ │ └── ICMCommonInlineVideos.m │ │ ├── Dailymotion/ │ │ │ ├── ICMDailymotion.h │ │ │ └── ICMDailymotion.m │ │ ├── Gyazo/ │ │ │ ├── ICMGyazo.h │ │ │ └── ICMGyazo.m │ │ ├── Imgur .gifv/ │ │ │ ├── ICMImgurGifv.h │ │ │ └── ICMImgurGifv.m │ │ ├── Pornhub/ │ │ │ ├── ICMPornhub.h │ │ │ └── ICMPornhub.m │ │ ├── Streamable/ │ │ │ ├── ICMStreamable.h │ │ │ └── ICMStreamable.m │ │ ├── Twitch Clips/ │ │ │ ├── ICMTwitchClips.h │ │ │ └── ICMTwitchClips.m │ │ ├── Twitch Live/ │ │ │ ├── ICMTwitchLive.h │ │ │ └── ICMTwitchLive.m │ │ ├── Twitter/ │ │ │ ├── ICMTweet.h │ │ │ └── ICMTweet.m │ │ ├── Vimeo/ │ │ │ ├── ICMVimeo.h │ │ │ └── ICMVimeo.m │ │ ├── YouTube/ │ │ │ ├── ICMYouTube.h │ │ │ └── ICMYouTube.m │ │ └── xkcd/ │ │ ├── ICMXkcd.h │ │ └── ICMXkcd.m │ ├── Inline Content Loader Core Media.xcodeproj/ │ │ └── project.pbxproj │ └── Resources/ │ ├── Modules/ │ │ ├── Dailymotion/ │ │ │ └── ICMDailymotion.mustache │ │ ├── Pornhub/ │ │ │ └── ICMPornhub.mustache │ │ ├── Twitch Clips/ │ │ │ └── ICMTwitchClips.mustache │ │ ├── Twitch Live/ │ │ │ └── ICMTwitchLive.mustache │ │ ├── Twitter/ │ │ │ └── ICMTweet.js │ │ ├── Vimeo/ │ │ │ └── ICMVimeo.mustache │ │ └── YouTube/ │ │ └── ICMYouTube.mustache │ └── Property Lists/ │ └── Info.plist ├── Inline Content Loader.xcodeproj/ │ └── project.pbxproj └── Resources/ ├── Modules/ │ ├── ICMInlineHTML.css │ ├── ICMInlineHTML.js │ ├── ICMInlineHTML.mustache │ ├── ICMInlineImage.css │ ├── ICMInlineImage.js │ ├── ICMInlineImage.mustache │ ├── ICMInlineVideo.css │ ├── ICMInlineVideo.js │ ├── ICMInlineVideo.mustache │ └── InlineImageLiveResize.js └── Property Lists/ └── Info.plist ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *~ *~.* .DS_Store .LSOverride *.mode1v3 *.mode2v3 *.pbxuser *.perspectivev3 *.pyc .tmp build Build Results xcuserdata *.xcworkspace Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/Installation Files/Extensions/* Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/Packages/MACAPPSTORE/* Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/Packages/STANDALONE/* !/Configurations/Build ================================================ FILE: .gitmodules ================================================ [submodule "Frameworks/Auto Hyperlinks"] path = Frameworks/Auto Hyperlinks url = https://github.com/Codeux-Software/Auto-Hyperlinks.git [submodule "Frameworks/Cocoa Extensions"] path = Frameworks/Cocoa Extensions url = https://github.com/Codeux-Software/Cocoa-Extensions.git [submodule "Frameworks/Encryption Kit"] path = Frameworks/Encryption Kit url = https://github.com/Codeux-Software/Encryption-Kit.git [submodule "Frameworks/Static Libraries"] path = Frameworks/Static Libraries url = https://github.com/Codeux-Software/Static-Libraries.git [submodule "Sources/Plugins/Sample Code/Swift Bot"] path = Sources/Plugins/Sample Code/Swift Bot url = https://github.com/Codeux/Textual-Swift-Plugin.git ================================================ FILE: Configurations/Build/Code Signing Identity.xcconfig ================================================ // This is the code signing identity used by all projects. CODE_SIGN_IDENTITY = Mac Developer // Set these values to empty DEVELOPMENT_TEAM = 8482Q6EPL6 PROVISIONING_PROFILE_SPECIFIER = 8482Q6EPL6/ ================================================ FILE: Configurations/Build/Common/Foundation Debug.xcconfig ================================================ // Foundation Debug.xcconfig acts as a base for ALL projects. // This configuration should NEVER contain any settings // which may be unique to a specific project. #include "Foundation.xcconfig" DEPLOYMENT_POSTPROCESSING = NO ENABLE_NS_ASSERTIONS = YES ENABLE_TESTABILITY = YES GCC_OPTIMIZATION_LEVEL = 0 GCC_PREPROCESSOR_DEFINITIONS = DEBUG ONLY_ACTIVE_ARCH = YES RUN_CLANG_STATIC_ANALYZER = NO SWIFT_COMPILATION_MODE = singlefile SWIFT_OPTIMIZATION_LEVEL = -Onone VERSION_INFO_BUILDER = ${USER} // // On Xcode 16, Textual failed to run in debug mode because // a "Textual.debug.dylib" file was missing in the @rpath // of extensions and services. Going to disable this feature // of Xcode until a later release of Xcode. // // From Xcode 16 beta 1 release notes: // // Some large or complex projects may fail to build and // run if they are scanning for specific Mach-O sections // in their binaries. (123416939) // // Workaround: Try setting the build setting ENABLE_DEBUG_DYLIB=NO. // This will disable the debug dylib that enables the new preview // execution mode. Setting this to NO will still allow you to // preview in Xcode 16 Seed 1 using the legacy execution mode, // but support for this mode will be removed in a future build. // ENABLE_DEBUG_DYLIB = NO ================================================ FILE: Configurations/Build/Common/Foundation.xcconfig ================================================ // Foundation.xcconfig acts as a base for ALL projects. // This configuration should NEVER contain any settings // which may be unique to a specific project. ALWAYS_SEARCH_USER_PATHS = NO APPLY_RULES_IN_COPY_FILES = YES ARCHS = $(ARCHS_STANDARD) CLANG_ANALYZER_GCD_PERFORMANCE = YES CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES CLANG_CXX_LANGUAGE_STANDARD = gnu++0x CLANG_ENABLE_MODULES = YES CLANG_ENABLE_OBJC_ARC = YES CLANG_USE_OPTIMIZATION_PROFILE = NO CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES CLANG_WARN_BOOL_CONVERSION = YES CLANG_WARN_COMMA = YES CLANG_WARN_CONSTANT_CONVERSION = YES CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES CLANG_WARN_EMPTY_BODY = YES CLANG_WARN_ENUM_CONVERSION = YES CLANG_WARN_INFINITE_RECURSION = YES CLANG_WARN_INT_CONVERSION = YES CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES CLANG_WARN_OBJC_LITERAL_CONVERSION = YES CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES CLANG_WARN_RANGE_LOOP_ANALYSIS = YES CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_SUSPICIOUS_MOVE = YES CLANG_WARN_UNREACHABLE_CODE = YES CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CODE_SIGN_ENTITLEMENTS = COMBINE_HIDPI_IMAGES = YES CONFIGURATION_BUILD_DIR = $(PROJECT_DIR)/Build Results/$(CONFIGURATION) COPY_PHASE_STRIP = NO DEAD_CODE_STRIPPING = YES DEBUG_INFORMATION_FORMAT = dwarf-with-dsym DEFINES_MODULE = YES DEPLOYMENT_POSTPROCESSING = YES ENABLE_HARDENED_RUNTIME = YES ENABLE_NS_ASSERTIONS = YES ENABLE_STRICT_OBJC_MSGSEND = NO FRAMEWORK_SEARCH_PATHS = GCC_C_LANGUAGE_STANDARD = gnu99 GCC_DYNAMIC_NO_PIC = YES GCC_ENABLE_OBJC_EXCEPTIONS = YES GCC_GENERATE_DEBUGGING_SYMBOLS = YES GCC_NO_COMMON_BLOCKS = YES GCC_OPTIMIZATION_LEVEL = fast GCC_PREFIX_HEADER = GCC_UNROLL_LOOPS = YES GCC_WARN_64_TO_32_BIT_CONVERSION = YES GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES GCC_WARN_ABOUT_MISSING_NEWLINE = YES GCC_WARN_ABOUT_RETURN_TYPE = YES GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = NO GCC_WARN_UNDECLARED_SELECTOR = NO GCC_WARN_UNINITIALIZED_AUTOS = YES GCC_WARN_UNUSED_FUNCTION = YES GCC_WARN_UNUSED_LABEL = YES GCC_WARN_UNUSED_VARIABLE = YES HEADER_SEARCH_PATHS = INFOPLIST_OUTPUT_FORMAT = binary LD_NO_PIE = YES LD_NO_PIE[arch=arm64] = NO LD_RUNPATH_SEARCH_PATHS = @loader_path/../Frameworks @executable_path/../Frameworks LIBRARY_SEARCH_PATHS = MACOSX_DEPLOYMENT_TARGET = 11.0 ONLY_ACTIVE_ARCH = NO OTHER_CFLAGS = -Xclang -fobjc-runtime-has-weak PLIST_FILE_OUTPUT_FORMAT = binary PRODUCT_BUNDLE_IDENTIFIER = REMOVE_HEADERS_FROM_EMBEDDED_BUNDLES = YES RUN_CLANG_STATIC_ANALYZER = NO STRINGS_FILE_OUTPUT_ENCODING = binary SWIFT_COMPILATION_MODE = wholemodule SWIFT_INSTALL_OBJC_HEADER = NO SWIFT_OPTIMIZATION_LEVEL = -O SWIFT_VERSION = 5.0 VERSION_INFO_BUILDER = Codeux Software, LLC WARNING_CFLAGS = -Wobjc-circular-container -Winfinite-recursion #include "../Code Signing Identity.xcconfig" ================================================ FILE: Configurations/Build/Common/Preserve Symbols.xcconfig ================================================ // It is important to preserve symbols and code that is // not directly used so that extensions can use them. DEAD_CODE_STRIPPING = NO GCC_SYMBOLS_PRIVATE_EXTERN = NO STRIP_STYLE = non-global ================================================ FILE: Configurations/Build/Common/Textual App.xcconfig ================================================ // Main application TEXTUAL_WORKSPACE_DIR = ${PROJECT_DIR}/../.. HEADER_SEARCH_PATHS = "${TEXTUAL_WORKSPACE_TEMP_DIR}/Build Headers/" "${TEXTUAL_WORKSPACE_DIR}/Sources/Shared/Headers/**" FRAMEWORK_SEARCH_PATHS = "${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks/**" "${TEXTUAL_WORKSPACE_DIR}/Frameworks/**" LIBRARY_SEARCH_PATHS = "${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks/**" "${TEXTUAL_WORKSPACE_DIR}/Frameworks/**" PRODUCT_BUNDLE_IDENTIFIER = ${TEXTUAL_BUNDLE_IDENTIFIER} PRODUCT_NAME = ${TEXTUAL_PRODUCT_NAME} WRAPPER_EXTENSION = app PRODUCT_MODULE_NAME = Textual CONFIGURATION_BUILD_DIR = $(TEXTUAL_WORKSPACE_DIR)/Build Results/$(CONFIGURATION) GCC_PREFIX_HEADER = ${PROJECT_DIR}/Classes/Headers/Private/TextualPrivate.h SWIFT_OBJC_BRIDGING_HEADER = ${PROJECT_DIR}/Classes/Headers/Private/SwiftBridgingHeaderPrivate.h ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES REMOVE_HEADERS_FROM_EMBEDDED_BUNDLES = NO INFOPLIST_FILE = ${TEXTUAL_WORKSPACE_TEMP_DIR}/Info.plist #include "Preserve Symbols.xcconfig" ================================================ FILE: Configurations/Build/Common/Textual Extensions.xcconfig ================================================ // Textual Extensions *.xcconfig file contains settings that // are specific to extensions for Textual (main application). // TEXTUAL_WORKSPACE_DIR is replaced by the build script TEXTUAL_WORKSPACE_DIR = ${PROJECT_DIR}/../../.. CONFIGURATION_BUILD_DIR = ${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Extensions CODE_SIGN_ENTITLEMENTS = ${TEXTUAL_WORKSPACE_DIR}/Configurations/Sandbox/Inherited.entitlements HEADER_SEARCH_PATHS = ${inherited} "${TEXTUAL_WORKSPACE_TEMP_DIR}/Build Headers/" "${TEXTUAL_WORKSPACE_DIR}/Sources/App/Classes/Headers/**" "${TEXTUAL_WORKSPACE_DIR}/Sources/Shared/Headers/**" LIBRARY_SEARCH_PATHS = ${inherited} "${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks/**" "${TEXTUAL_WORKSPACE_DIR}/Frameworks/**" FRAMEWORK_SEARCH_PATHS = ${inherited} "${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks/**" "${TEXTUAL_WORKSPACE_DIR}/Frameworks/**" WRAPPER_EXTENSION = bundle PROVISIONING_PROFILE = PROVISIONING_PROFILE_SPECIFIER = BUNDLE_LOADER = ${TEXTUAL_PRODUCT_BINARY} ================================================ FILE: Configurations/Build/Common/Textual.xcconfig ================================================ // Main application, extensions, and XPC services TEXTUAL_PRODUCT_NAME = Textual // TEXTUAL_PRODUCT_LOCATION is replaced by the build script TEXTUAL_PRODUCT_LOCATION = /Applications/${TEXTUAL_PRODUCT_NAME}.app // TEXTUAL_PRODUCT_BINARY is replaced by the build script TEXTUAL_PRODUCT_BINARY = ${TEXTUAL_PRODUCT_LOCATION}/Contents/MacOS/${TEXTUAL_PRODUCT_NAME} // TEXTUAL_WORKSPACE_TEMP_DIR should be same for every // project so only override TEXTUAL_WORKSPACE_DIR, not this. TEXTUAL_WORKSPACE_TEMP_DIR = ${TEXTUAL_WORKSPACE_DIR}/.tmp SHARED_PRECOMPS_DIR = ${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedPCH DWARF_DSYM_FOLDER_PATH = ${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedSymbols // TEXTUAL_BUILT_AS_UNIVERSAL_BINARY acts an easy way to // modify code based on the type of binary being built. // As with other feature flags, changing this value only // adds or excludes certain chunks of code. It doesn't // change the underlying build process. TEXTUAL_BUILT_AS_UNIVERSAL_BINARY = 1 ================================================ FILE: Configurations/Build/Common/XPC Service - ICL Extensions.xcconfig ================================================ // Inline Content Loader XPC service extensions // TEXTUAL_WORKSPACE_DIR is replaced by the build script TEXTUAL_WORKSPACE_DIR = ${PROJECT_DIR}/../../../.. // ICL_WORKSPACE_DIR is NOT replaced by the build script ICL_WORKSPACE_DIR = ${TEXTUAL_WORKSPACE_DIR}/XPC Services/Inline Content Loader // ICL_PRODUCT_LOCATION is replaced by the build script ICL_PRODUCT_LOCATION = ${TEXTUAL_PRODUCT_LOCATION}/Contents/XPCServices/InlineContentLoader.xpc // ICL_PRODUCT_BINARY is replaced by the build script ICL_PRODUCT_BINARY = ${ICL_PRODUCT_LOCATION}/Contents/MacOS/InlineContentLoader CONFIGURATION_BUILD_DIR = ${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-ICLExtension HEADER_SEARCH_PATHS = ${inherited} "${ICL_WORKSPACE_DIR}/Classes/Headers/**" "${ICL_WORKSPACE_DIR}/Classes/Modules/**" "${TEXTUAL_WORKSPACE_DIR}/Sources/Shared/Headers/**" LIBRARY_SEARCH_PATHS = ${inherited} "${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks/**" "${TEXTUAL_WORKSPACE_DIR}/Frameworks/**" FRAMEWORK_SEARCH_PATHS = ${inherited} "${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks/**" "${TEXTUAL_WORKSPACE_DIR}/Frameworks/**" WRAPPER_EXTENSION = bundle PROVISIONING_PROFILE = PROVISIONING_PROFILE_SPECIFIER = BUNDLE_LOADER = ${ICL_PRODUCT_BINARY} ================================================ FILE: Configurations/Build/Common/XPC Service - ICL.xcconfig ================================================ // Inline content loader XPC service #include "Preserve Symbols.xcconfig" ================================================ FILE: Configurations/Build/Common/XPC Services.xcconfig ================================================ // XPC services // TEXTUAL_WORKSPACE_DIR is replaced by the build script TEXTUAL_WORKSPACE_DIR = ${PROJECT_DIR}/../.. CONFIGURATION_BUILD_DIR = ${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-XPCServices CODE_SIGN_ENTITLEMENTS = ${TEXTUAL_WORKSPACE_DIR}/Configurations/Sandbox/Inherited.entitlements HEADER_SEARCH_PATHS = $(inherited) "${TEXTUAL_WORKSPACE_TEMP_DIR}/Build Headers/" "${TEXTUAL_WORKSPACE_DIR}/Sources/Shared/Headers/**" LIBRARY_SEARCH_PATHS = $(inherited) "${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks/**" "${TEXTUAL_WORKSPACE_DIR}/Frameworks/**" FRAMEWORK_SEARCH_PATHS = $(inherited) "${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks/**" "${TEXTUAL_WORKSPACE_DIR}/Frameworks/**" WRAPPER_EXTENSION = xpc GCC_PREPROCESSOR_DEFINITIONS = $(inherited) TEXTUAL_BUILDING_XPC_SERVICE=1 // Link against frameworks in app itself rather than creating copies // ./(self) ../(Contents) ../(.xpc bundle) ../(XPCServices) ../(Contents) LD_RUNPATH_SEARCH_PATHS = $(inherited) @loader_path/../../../../Frameworks ================================================ FILE: Configurations/Build/Debug/Enabled Features.xcconfig ================================================ // The main project of Textual, its extensions, and its XPC services // all inherit one of these configuration files to allow them to // selectively disable code based on which features are enabled. // These macros that are used to disable and enable the inclusion // of certain segments of code. Toggling one of these flags does not // guarantee that the toggled feature will work. These only toggle the code. // Not add additional resource files that the code may be dependent on. // To add new feature flags, modify UpdateFeatureFlags.sh TEXTUAL_BUILT_INSIDE_SANDBOX=1 TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION=1 TEXTUAL_BUILT_WITH_LICENSE_MANAGER=1 ================================================ FILE: Configurations/Build/Debug/Textual App.xcconfig ================================================ #include "../Common/Foundation Debug.xcconfig" #include "Textual.xcconfig" #include "../Common/Textual App.xcconfig" CODE_SIGN_ENTITLEMENTS = ${PROJECT_DIR}/Configurations/Sandbox/Debug.entitlements ================================================ FILE: Configurations/Build/Debug/Textual Extensions.xcconfig ================================================ #include "../Common/Foundation Debug.xcconfig" #include "Textual.xcconfig" #include "../Common/Textual Extensions.xcconfig" ================================================ FILE: Configurations/Build/Debug/Textual.xcconfig ================================================ #include "../Common/Textual.xcconfig" #include "Enabled Features.xcconfig" TEXTUAL_BUNDLE_IDENTIFIER = com.codeux.apps.textual TEXTUAL_GROUP_CONTAINER_IDENTIFIER = 8482Q6EPL6.com.codeux.apps.textual TEXTUAL_BUILD_SCHEME_TOKEN = debug TEXTUAL_EXTENSION_BUILD_SCHEME = Debug TEXTUAL_FRAMEWORK_BUILD_SCHEME = Release // Frameworks don't have a Debug scheme TEXTUAL_XPC_SERVICE_BUILD_SCHEME = Debug ================================================ FILE: Configurations/Build/Debug/XPC Service - ICL Extensions.xcconfig ================================================ #include "../Common/Foundation Debug.xcconfig" #include "Textual.xcconfig" #include "../Common/XPC Service - ICL Extensions.xcconfig" ================================================ FILE: Configurations/Build/Debug/XPC Service - ICL.xcconfig ================================================ #include "XPC Services.xcconfig" #include "../Common/XPC Service - ICL.xcconfig" ICL_EXTENSION_BUILD_SCHEME = Debug ================================================ FILE: Configurations/Build/Debug/XPC Services.xcconfig ================================================ #include "../Common/Foundation Debug.xcconfig" #include "Textual.xcconfig" #include "../Common/XPC Services.xcconfig" ================================================ FILE: Configurations/Build/README.md ================================================ TODO: Write a README ================================================ FILE: Configurations/Build/Standard Release/Enabled Features.xcconfig ================================================ // The main project of Textual, its extensions, and its XPC services // all inherit one of these configuration files to allow them to // selectively disable code based on which features are enabled. // These macros that are used to disable and enable the inclusion // of certain segments of code. Toggling one of these flags does not // guarantee that the toggled feature will work. These only toggle the code. // Not add additional resource files that the code may be dependent on. // To add new feature flags, modify UpdateFeatureFlags.sh TEXTUAL_BUILT_INSIDE_SANDBOX=1 TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION=1 TEXTUAL_BUILT_WITH_LICENSE_MANAGER=1 TEXTUAL_BUILT_WITH_SPARKLE_ENABLED=1 ================================================ FILE: Configurations/Build/Standard Release/Textual App.xcconfig ================================================ #include "../Common/Foundation.xcconfig" #include "Textual.xcconfig" #include "../Common/Textual App.xcconfig" CODE_SIGN_ENTITLEMENTS = ${PROJECT_DIR}/Configurations/Sandbox/Standard Release.entitlements ================================================ FILE: Configurations/Build/Standard Release/Textual Extensions.xcconfig ================================================ #include "../Common/Foundation.xcconfig" #include "Textual.xcconfig" #include "../Common/Textual Extensions.xcconfig" ================================================ FILE: Configurations/Build/Standard Release/Textual.xcconfig ================================================ #include "../Common/Textual.xcconfig" #include "Enabled Features.xcconfig" TEXTUAL_BUNDLE_IDENTIFIER = com.codeux.apps.textual TEXTUAL_GROUP_CONTAINER_IDENTIFIER = 8482Q6EPL6.com.codeux.apps.textual TEXTUAL_BUILD_SCHEME_TOKEN = devid TEXTUAL_EXTENSION_BUILD_SCHEME = Release TEXTUAL_FRAMEWORK_BUILD_SCHEME = Release TEXTUAL_XPC_SERVICE_BUILD_SCHEME = Release ================================================ FILE: Configurations/Build/Standard Release/XPC Service - ICL Extensions.xcconfig ================================================ #include "../Common/Foundation.xcconfig" #include "Textual.xcconfig" #include "../Common/XPC Service - ICL Extensions.xcconfig" ================================================ FILE: Configurations/Build/Standard Release/XPC Service - ICL.xcconfig ================================================ #include "XPC Services.xcconfig" #include "../Common/XPC Service - ICL.xcconfig" ICL_EXTENSION_BUILD_SCHEME = Release ================================================ FILE: Configurations/Build/Standard Release/XPC Services.xcconfig ================================================ #include "../Common/Foundation.xcconfig" #include "Textual.xcconfig" #include "../Common/XPC Services.xcconfig" ================================================ FILE: Configurations/ExportArchiveConfiguration.plist ================================================ teamID 8482Q6EPL6 method developer-id ================================================ FILE: Configurations/Sandbox/Inherited.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.inherit ================================================ FILE: README.md ================================================ > [!IMPORTANT] > Textual is no longer being actively maintained. For the life of the project, it only ever had one full time maintainer whom has now moved on to different ventures in their life. To all that have contributed to Textual in some form in the past; be it a suggestion, bug report, pull request, financial support, or some other form of contribvution, you will forever be loved. Thank you so much. Words cannot properly express the gratitude we have for every single user. # Textual [![GitHub release](https://img.shields.io/github/tag/Codeux-Software/Textual.svg)](https://github.com/Codeux-Software/Textual/blob/master) [![Platform](https://img.shields.io/badge/platform-OS%20X-lightgrey.svg)](http://www.textualapp.com/mac-app-store) Textual is a highly customizable app for interacting with Internet Relay Chat (IRC) chatrooms on macOS. Textual can be customized with styles written in CSS, HTML, and JavaScript; [plugins](https://help.codeux.com/textual/Writing-Plugins.kb) written in Objective-C & Swift, and [scripts](https://help.codeux.com/textual/Writing-Scripts.kb) written in AppleScript (plus many other languages) Precompiled versions of Textual can be purchased in the [directly from codeux.com](https://www.textualapp.com/). ## Screenshots [![Light Screenshot](https://www.codeux.com/textual/private/images/v600media/YosemiteLightThumbnail.png)](https://www.codeux.com/textual/private/images/v600media/YosemiteLightFullscreen.png) [![Dark Screenshot](https://www.codeux.com/textual/private/images/v600media/YosemiteDarkThumbnail.png)](https://www.codeux.com/textual/private/images/v600media/YosemiteDarkFullscreen.png) ## Resources - [Homepage](https://codeux.com/textual) - [Frequently Asked Questions](https://help.codeux.com/textual/Frequently-Asked-Questions.kb) - [Support](https://help.codeux.com/textual/Support.kb) - \#textual on irc.libera.chat - Guides: [Writing Plugins](https://help.codeux.com/textual/Writing-Plugins.kb), [Writing Scripts](https://help.codeux.com/textual/Writing-Scripts.kb) ## Note Regarding Downloading Source Code Textual is dependent on several other projects to build. This repository is automatically linked against these other projects using what are known as "submodules" — Clicking the "Download ZIP" button to build a copy of Textual will not download a copy of these projects. The source code must be cloned using [Github for Mac](https://mac.github.com/) or by using the following commands in Terminal: ``` git clone https://github.com/Codeux-Software/Textual.git Textual cd Textual git submodule update --init --recursive ``` ## Note Regarding Code Signing **DO NOT change the Code Signing Identity setting through Xcode.** Textual uses a configuration file to specify the code signing identity. This allows it to be used across all projects associated with Textual without having to modify each. **DO** edit the file located at _[Configurations ➜ Build ➜ Code Signing Identity.xcconfig](https://github.com/Codeux-Software/Textual/blob/master/Configurations/Build/Code%20Signing%20Identity.xcconfig)_ **It is HIGHLY DISCOURAGED to turn off code signing.** Certain features rely on the fact that Textual is properly signed and is within a sandboxed environment. **TEXTUAL DOES NOT REQUIRE A CERTIFICATE ISSUED BY APPLE TO BUILD** which means there is absolutely no reason to turn code signing off. ## Note Regarding Trial Mode The code which is responsible for licensing paid copies of Textual is in the source code that you download from here. If you do not have a license key, then set the ``TEXTUAL_BUILT_WITH_LICENSE_MANAGER`` flag to `0` in the `Standard Release` configuration file to disable the inclusion of this code at build time. ## Building Textual The latest version of Textual requires two things to be built. One is a valid (does not need to be issued by Apple) code signing certificate. The second is an installation of Xcode 10.0 or newer on macOS High Sierra. **Building on anything earlier is not supported because of Swift 4.2 code.** **DO NOT change the Code Signing Identity setting through Xcode.** Modify the file located at _[Configurations ➜ Build ➜ Code Signing Identity.xcconfig](https://github.com/Codeux-Software/Textual/blob/master/Configurations/Build/Code%20Signing%20Identity.xcconfig)_ instead. Build Textual using the "Standard Release" build scheme. ## Original Limechat License Textual began as a fork of [LimeChat](https://github.com/psychs/limechat) in 2010 LimeChat's original license is presented below.
The New BSD License

Copyright (c) 2008 - 2010 Satoshi Nakagawa < psychs AT limechat DOT net >
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
## License for content originating from Textual Unless stated otherwise by Textual's [Acknowledgements.pdf](Acknowledgements.pdf) document, the license presented below shall govern the distribution of and modifications to; the work hosted by this repository.
Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors.
      Please see Acknowledgements.pdf for additional information.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

   * Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.
   * Neither the name of Textual, "Codeux Software, LLC", nor the
     names of its contributors may be used to endorse or promote products
     derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
================================================ FILE: Sources/App/Build Scripts/BuildExtensions.sh ================================================ #!/bin/bash set -e echo "Building using architecture: ${ARCHS}" TEXTUAL_PRODUCT_LOCATION="${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" TEXTUAL_PRODUCT_BINARY="${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}" plugins=( 'Caffeine' 'Chat Filter' 'Smiley Converter' 'System Profiler' 'User Insights' 'ZNC Additions' ) for plugin in "${plugins[@]}"; do cd "${TEXTUAL_WORKSPACE_DIR}/Sources/Plugins/${plugin}" xcodebuild -target "$plugin Extension" \ -configuration "${TEXTUAL_EXTENSION_BUILD_SCHEME}" \ ARCHS="${ARCHS}" \ CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}" \ DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" \ PROVISIONING_PROFILE_SPECIFIER="" \ TEXTUAL_WORKSPACE_DIR="${TEXTUAL_WORKSPACE_DIR}" \ TEXTUAL_PRODUCT_LOCATION="${TEXTUAL_PRODUCT_LOCATION}" \ TEXTUAL_PRODUCT_BINARY="${TEXTUAL_PRODUCT_BINARY}" done exit 0 ================================================ FILE: Sources/App/Build Scripts/BuildFrameworks.sh ================================================ #!/bin/bash set -e echo "Building using architecture: ${ARCHS}" CONFIGURATION_BUILD_DIR="${TEXTUAL_WORKSPACE_TEMP_DIR}/SharedBuildProducts-Frameworks" xcb() { target=$1 xcodebuild -target "$target" \ -configuration "${TEXTUAL_FRAMEWORK_BUILD_SCHEME}" \ ARCHS="${ARCHS}" \ CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}" \ CONFIGURATION_BUILD_DIR="${CONFIGURATION_BUILD_DIR}" \ DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" \ PROVISIONING_PROFILE_SPECIFIER="" \ DWARF_DSYM_FOLDER_PATH="${DWARF_DSYM_FOLDER_PATH}" } # Assumes the name and filename of the framework is the same just without spaces. frameworks=( 'Auto Hyperlinks' 'Encryption Kit' 'Cocoa Extensions' ) for framework in "${frameworks[@]}"; do cd "${TEXTUAL_WORKSPACE_DIR}/Frameworks/${framework}/" xcb "${framework// /}.framework" done exit 0 ================================================ FILE: Sources/App/Build Scripts/BuildServices.sh ================================================ #!/bin/bash set -e echo "Building using architecture: ${ARCHS}" TEXTUAL_PRODUCT_LOCATION="${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" TEXTUAL_PRODUCT_BINARY="${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}" services=( 'Historic Log File Manager' 'Inline Content Loader' 'IRC Remote Connection Manager' ) for service in "${services[@]}"; do cd "${TEXTUAL_WORKSPACE_DIR}/XPC Services/${service}/" xcodebuild -target "$service" \ -configuration "${TEXTUAL_XPC_SERVICE_BUILD_SCHEME}" \ ARCHS="${ARCHS}" \ CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}" \ DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" \ PROVISIONING_PROFILE_SPECIFIER="" \ TEXTUAL_WORKSPACE_DIR="${TEXTUAL_WORKSPACE_DIR}" \ TEXTUAL_PRODUCT_LOCATION="${TEXTUAL_PRODUCT_LOCATION}" \ TEXTUAL_PRODUCT_BINARY="${TEXTUAL_PRODUCT_BINARY}" done ================================================ FILE: Sources/App/Build Scripts/ExportArchive.sh ================================================ #!/bin/sh set -e WORKING_PATH="${TEXTUAL_WORKSPACE_TEMP_DIR}/ArchiveTan" mkdir -p "${WORKING_PATH}" cd "${WORKING_PATH}" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ xcodebuild -exportArchive \ -exportOptionsPlist "${TEXTUAL_WORKSPACE_DIR}/Configurations/ExportArchiveConfiguration.plist" \ -archivePath "${ARCHIVE_PATH}" \ -exportPath "${WORKING_PATH}" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Zip product and send to notary # # Format to add notary to keychain: # # xcrun notarytool store-credentials "Textual Notary" # --apple-id "" # --team-id # --password "" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ WORKING_ZIP_PATH="./${FULL_PRODUCT_NAME}.zip" zip -y -r -X "${WORKING_ZIP_PATH}" "./${FULL_PRODUCT_NAME}/" xcrun notarytool submit "${WORKING_ZIP_PATH}" \ --keychain-profile "Textual Notary" \ --wait \ --verbose \ --progress # Remove uploaded product rm "${WORKING_ZIP_PATH}" # Stable app xcrun stapler staple --verbose "./${FULL_PRODUCT_NAME}/" # Create new zip with stapled app zip -y -r -X "${WORKING_ZIP_PATH}" "./${FULL_PRODUCT_NAME}/" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Call `git` after `cd` into working path to make # sure we are in a directory of a git repository. GIT_COMMIT_HASH=`git rev-parse --short HEAD` EXPORT_PATH="${HOME}/Desktop/Textual-${GIT_COMMIT_HASH}" mkdir -p "${EXPORT_PATH}" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # if [ "${TEXTUAL_BUILT_AS_UNIVERSAL_BINARY}" == "1" ]; then ARCHSPEC_PATH="${EXPORT_PATH}/universal" else ARCHSPEC_PATH="${EXPORT_PATH}/intel" fi mkdir -p "${ARCHSPEC_PATH}" ZIP_EXPORT_PATH="${ARCHSPEC_PATH}/Textual.zip" mv "${WORKING_ZIP_PATH}" "${ZIP_EXPORT_PATH}" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # BUNDLE_VERSION_LONG=$(/usr/libexec/PlistBuddy -c "Print \"CFBundleVersion\"" "./${FULL_PRODUCT_NAME}/Contents/Info.plist") BUNDLE_VERSION_SHORT=$(/usr/libexec/PlistBuddy -c "Print \"CFBundleShortVersionString\"" "./${FULL_PRODUCT_NAME}/Contents/Info.plist") BUNDLE_MINIMUM_TARGET=$(/usr/libexec/PlistBuddy -c "Print \"LSMinimumSystemVersion\"" "./${FULL_PRODUCT_NAME}/Contents/Info.plist") echo " ./buildInfo.php echo " \$current_release_version_short = \"${BUNDLE_VERSION_SHORT}\";" >> ./buildInfo.php echo " \$current_release_version_long = \"${BUNDLE_VERSION_LONG}\";" >> ./buildInfo.php echo " \$current_release_version_signature = \"${GIT_COMMIT_HASH}\";" >> ./buildInfo.php echo " \$current_release_minimum_system_version = \"${BUNDLE_MINIMUM_TARGET}\";" >> buildInfo.php mv "./buildInfo.php" "${EXPORT_PATH}" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # echo "
Release Notes for ${BUNDLE_VERSION_LONG}
    " > ./buildLog.txt git log --since='48 hours ago' --pretty=format:'
  • %s
  • ' >> ./buildLog.txt echo "
" >> ./buildLog.txt mv "./buildLog.txt" "${EXPORT_PATH}" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # cd "${DWARF_DSYM_FOLDER_PATH}" DYSM_EXPORT_PATH="${ARCHSPEC_PATH}/Debug symbols.zip" zip -y -r -X "${DYSM_EXPORT_PATH}" * # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # rm -rf "${WORKING_PATH}" exit 0; ================================================ FILE: Sources/App/Build Scripts/MergeSwift.sh ================================================ #!/bin/sh set -e # # Textual and Remote Connection Manager service both use Swift. # Textual embeds the library and the service will inherit that. # For some reason, during development, I found that the service # would not launch because "libswiftSwiftOnoneSupport.dylib" # was missing in Textual's copy of the library. # # Textual and the service both declare -Onone optimization level # at the same time, so I don't know why one will have it and # the other does not. # # To workaround this issue and any in the future, this script exists. # This script will find all libswift*.dylb files that aren't in the # main copy of the library and bring those files in. # # TODO: Revisit this when I've had sleep (June 28, 2018) # cd "${TARGET_BUILD_DIR}/${CONTENTS_FOLDER_PATH}" find . -type f -name "libswift*" \ -not -path "./Frameworks/*" \ -not -path "./Resources/*" \ -not -path "*/Resources/libswiftRemoteMirror.dylib" \ -exec cp '{}' ./Frameworks/ \; \ -exec rm '{}' \; exit 0; ================================================ FILE: Sources/App/Build Scripts/PostprocessSparkle.sh ================================================ #!/bin/bash set -e echo "Performing postprocessing on Sparkle framework" cd "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" rm -rf Sparkle.framework/Versions/B/XPCServices/Downloader.xpc codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime Sparkle.framework/Versions/B/XPCServices/Installer.xpc codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime Sparkle.framework/Versions/B/Autoupdate codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime Sparkle.framework/Versions/B/Updater.app codesign -f -s "$CODE_SIGN_IDENTITY" -o runtime Sparkle.framework exit 0 ================================================ FILE: Sources/App/Build Scripts/UpdateFeatureFlags.sh ================================================ #!/bin/bash set -e cd "${TEXTUAL_WORKSPACE_TEMP_DIR}/Build Headers/" echo " /* ANY CHANGES TO THIS FILE WILL NOT BE SAVED AND WILL NOT BE COMMITTED */ " > _FeatureFlags.h featureNames=("TEXTUAL_BUILT_INSIDE_SANDBOX" "TEXTUAL_BUILT_WITH_SPARKLE_ENABLED" "TEXTUAL_BUILT_WITH_LICENSE_MANAGER" "TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION" "TEXTUAL_BUILT_FOR_APP_STORE_DISTRIBUTION" "TEXTUAL_BUILT_AS_UNIVERSAL_BINARY") for feature in "${featureNames[@]}"; do featureValue="${!feature}" if [ -n "${featureValue}" ]; then echo "#define ${feature} ${featureValue}" >> _FeatureFlags.h else echo "#define ${feature} 0" >> _FeatureFlags.h fi done if cmp -s "FeatureFlags.h" "_FeatureFlags.h"; then echo "The feature flags file hasn't changed. Not deploying." rm "_FeatureFlags.h" else # Force flag is used on rm to avoid error for missing file rm -f "FeatureFlags.h" mv "_FeatureFlags.h" "FeatureFlags.h" fi # ------ # # Exit with success exit 0; ================================================ FILE: Sources/App/Build Scripts/UpdateVersionInfo.sh ================================================ #!/bin/sh set -e cd "${TEXTUAL_WORKSPACE_TEMP_DIR}/" # Make a copy of the Info.plist file in the .tmp folder # This will be the Info.plist file manipulated with the version # information that is generated below. infoPlistSource="${PROJECT_DIR}/Resources/Property Lists/Application Properties/Info.plist" infoPlistTarget="${TEXTUAL_WORKSPACE_TEMP_DIR}/Info.plist" if [ ! -f "${infoPlistTarget}" ] || [ "${infoPlistTarget}" -ot "${infoPlistSource}" ]; then echo "Step 1: Info.plist file doesn't exist and/or is oudated. Performing copy." # Copy with -p flag to preserve modification time cp -p "${infoPlistSource}" "${infoPlistTarget}" else echo "Step 1: Info.plist file hasn't changed." fi # Write the version information to the Info.plist file. # The build version is the date of the last commit in git. gitBundle=`which git` if [ -z "${gitBundle}" ]; then bundleVersionNew="000000.00" else gitDateOfLastCommit=`"${gitBundle}" log -n1 --format="%at"` bundleVersionNew=`/bin/date -u -r "${gitDateOfLastCommit}" "+%y%m%d.%H"` fi; bundleVersionOld=$(/usr/libexec/PlistBuddy -c "Print \"CFBundleVersion\"" Info.plist) if [ "${bundleVersionOld}" != "${bundleVersionNew}" ]; then echo "Step 2: Writing version: New ('${bundleVersionNew}'), Old ('${bundleVersionOld}')" /usr/libexec/PlistBuddy -c "Set \"CFBundleVersion\" \"${bundleVersionNew}\"" Info.plist else echo "Step 2: The version hasn't changed." fi # ------ # # Gather the information necessary for building Textual's BuildConfig.h # header. This header file gives various section of the code base version # information so it does not need to constantly access the Info.plist file. bundleVersionShort=$(/usr/libexec/PlistBuddy -c "Print \"CFBundleShortVersionString\"" Info.plist) mkdir -p "./Build Headers" cd "./Build Headers" echo " /* ANY CHANGES TO THIS FILE WILL NOT BE SAVED AND WILL NOT BE COMMITTED */ #define TXBundleBuildProductName @\"${PRODUCT_NAME}\" #define TXBundleBuildProductIdentifier @\"${PRODUCT_BUNDLE_IDENTIFIER}\" #define TXBundleBuildProductIdentifierCString \"${PRODUCT_BUNDLE_IDENTIFIER}\" #define TXBundleBuildGroupContainerIdentifier @\"${TEXTUAL_GROUP_CONTAINER_IDENTIFIER}\" #define TXBundleBuildVersion @\"${bundleVersionNew}\" #define TXBundleBuildVersionShort @\"${bundleVersionShort}\" #define TXBundleBuildScheme @\"${TEXTUAL_BUILD_SCHEME_TOKEN}\" " > _BuildConfig.h if [ -z "$CODE_SIGN_IDENTITY" ]; then echo "#define TXBundleBuiltWithoutCodeSigning 1" >> _BuildConfig.h fi if cmp -s "BuildConfig.h" "_BuildConfig.h"; then echo "Step 3: The build configuration file hasn't changed. Not deploying." rm "_BuildConfig.h" else # Force flag is used on rm to avoid error for missing file rm -f "BuildConfig.h" mv "_BuildConfig.h" "BuildConfig.h" fi # ------ # # Compile list of enabled features exec "${PROJECT_DIR}/Build Scripts/UpdateFeatureFlags.sh" > "${TEXTUAL_WORKSPACE_TEMP_DIR}/Script-Logs/UpdateFeatureFlags.txt" # ------ # # Exit with success exit 0; ================================================ FILE: Sources/App/Classes/Controllers/TXAppearance.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual and/or Codeux Software, nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TPCPreferencesLocal.h" #import "TXAppearance.h" NS_ASSUME_NONNULL_BEGIN NSString * const TXApplicationAppearanceChangedNotification = @"TXApplicationAppearanceChangedNotification"; NSString * const TXSystemAppearanceChangedNotification = @"TXSystemAppearanceChangedNotification"; @interface TXAppearancePropertyCollection () @property (nonatomic, copy, readwrite) NSString *appearanceName; @property (nonatomic, assign, readwrite) TXAppearanceType appearanceType; @property (nonatomic, assign, readwrite) BOOL isDarkAppearance; @property (nonatomic, assign, readwrite) TXAppKitAppearanceTarget appKitAppearanceTarget; @end @interface TXAppearance () @property (nonatomic, strong, readwrite) TXAppearancePropertyCollection *properties; @end @implementation TXAppearance #pragma mark - #pragma mark Initialization - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [self updateAppearance]; [RZWorkspaceNotificationCenter() addObserver:self selector:@selector(accessibilityDisplayOptionsDidChange:) name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification object:nil]; [NSApp addObserver:self forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew context:NULL]; } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Removing appearance change observers"); [RZNotificationCenter() removeObserver:self]; [NSApp removeObserver:self forKeyPath:@"effectiveAppearance"]; } #pragma mark - #pragma mark Properties + (nullable NSString *)appearanceNameForType:(TXAppearanceType)type { switch (type) { case TXAppearanceTypeBigSurLight: { return @"BigSurLight"; } case TXAppearanceTypeBigSurDark: { return @"BigSurDark"; } default: { return nil; } } } #pragma mark - #pragma mark Notifications - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:@"effectiveAppearance"]) { [self applicationAppearanceChanged]; } } - (void)applicationAppearanceChanged { /* Wait until next pass of the run loop to perform update because the effective appearance may not be propagated to all subviews when this is called. */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [self updateAppearanceBySystemChange]; }); } - (void)systemColorsDidChange:(NSNotification *)aNote { [self updateAppearanceBySystemChange]; } - (void)accessibilityDisplayOptionsDidChange:(NSNotification *)aNote { [self updateAppearanceBySystemChange]; } - (void)updateAppearance { [self updateAppearanceBySystemChange:NO]; } - (void)updateAppearanceBySystemChange { [self updateAppearanceBySystemChange:YES]; } - (void)updateAppearanceBySystemChange:(BOOL)systemChanged { TXAppearanceType appearanceType = TXAppearanceTypeBigSurLight; TXPreferredAppearance preferredAppearance = [TPCPreferences appearance]; /* Determine user's preference */ switch (preferredAppearance) { case TXPreferredAppearanceInherited: { if ([TXAppearancePropertyCollection systemWideDarkModeEnabled]) { appearanceType = TXAppearanceTypeBigSurDark; } break; } case TXPreferredAppearanceDark: { appearanceType = TXAppearanceTypeBigSurDark; break; } default: { break; } } BOOL isAppearanceDark = (appearanceType == TXAppearanceTypeBigSurDark); /* Determine best appearance inheritance approach */ /* Before Mojave, appearance needs to be applied to every view. After Mojave, views properly inherit the appearance of the parent window or the system. If the user selects a specific appearance, then we apply that to the parent window on Mojave and later to allow views to inherit that. If the user selects the system appearance, then we apply nothing and allow it to be inherited from the system. */ TXAppKitAppearanceTarget appKitAppearanceTarget = TXAppKitAppearanceTargetNone; if (preferredAppearance != TXPreferredAppearanceInherited) { appKitAppearanceTarget = TXAppKitAppearanceTargetWindow; } /* Test for changes */ TXAppearancePropertyCollection *oldProperties = self.properties; BOOL changeAppearance = (oldProperties == nil || (oldProperties.appearanceType != appearanceType) || (oldProperties.appKitAppearanceTarget != appKitAppearanceTarget)); if (changeAppearance == NO) { /* Even if the desired appearance hasn't changed, we still signal views to perform selection update so that vibrant views can draw correctly when the system changes. */ if (systemChanged == NO) { return; } } else { /* When appearance changes as a result of a system change, then we treat it as an application change as that's more specialized. */ systemChanged = NO; } /* Assign new properties */ TXAppearancePropertyCollection *newProperties = [TXAppearancePropertyCollection new]; newProperties.appearanceName = [self.class appearanceNameForType:appearanceType]; newProperties.appearanceType = appearanceType; newProperties.isDarkAppearance = isAppearanceDark; newProperties.appKitAppearanceTarget = appKitAppearanceTarget; self.properties = newProperties; /* Notify observers */ if (systemChanged == NO) { [self notifyApplicationAppearanceChanged]; } else { [self notifySystemAppearanceChanged]; } } - (void)notifyApplicationAppearanceChanged { [RZNotificationCenter() postNotificationName:TXApplicationAppearanceChangedNotification object:self]; } - (void)notifySystemAppearanceChanged { [RZNotificationCenter() postNotificationName:TXSystemAppearanceChangedNotification object:self]; } @end #pragma mark - #pragma mark Property Collection @implementation TXAppearancePropertyCollection - (nullable NSAppearance *)appKitAppearance { if (self.appKitAppearanceTarget == TXAppKitAppearanceTargetNone) { return nil; } if (self.isDarkAppearance) { return [self.class appKitDarkAppearance]; } else { return [self.class appKitLightAppearance]; } } - (NSString *)shortAppearanceDescription { if (self.isDarkAppearance == NO) { return @"light"; } else { return @"dark"; } } + (BOOL)systemWideDarkModeEnabled { return ([[NSApp effectiveAppearance] bestMatchFromAppearancesWithNames:@[NSAppearanceNameDarkAqua]] != nil); } + (nullable NSAppearance *)appKitDarkAppearance { return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; } + (nullable NSAppearance *)appKitLightAppearance { return [NSAppearance appearanceNamed:NSAppearanceNameAqua]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Controllers/TXApplication.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCAlert.h" #import "TLOLocalization.h" #import "TPCApplicationInfo.h" #import "TXApplicationPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TXApplication + (BOOL)checkForOtherCopiesOfTextualRunning { pid_t ourProcessIdentifier = [[NSProcessInfo processInfo] processIdentifier]; for (NSRunningApplication *application in RZWorkspace().runningApplications) { if ([application.bundleIdentifier isEqualToString:@"com.codeux.apps.textual"] || [application.bundleIdentifier isEqualToString:@"com.codeux.apps.textual-mas"] || [application.bundleIdentifier isEqualToString:@"com.codeux.irc.textual5"]) { if (application.processIdentifier == ourProcessIdentifier) { continue; } BOOL continueLaunch = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[kx4-q8]") title:TXTLS(@"Prompts[hcb-3i]") defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]")]; return continueLaunch; } } return YES; } - (void)sendEvent:(NSEvent *)event { BOOL performedCustomEvent = [self performedCustomKeyboardEvent:event]; if (performedCustomEvent) { return; } [super sendEvent:event]; } - (BOOL)performedCustomKeyboardEvent:(NSEvent *)event { if (event.type != NSEventTypeKeyDown) { return NO; } NSWindow *keyWindow = self.keyWindow; if ([self sendCustomKeyboardEvent:event toObject:keyWindow]) { return YES; } NSResponder *firstResponder = keyWindow.firstResponder; if ([self sendCustomKeyboardEvent:event toObject:firstResponder]) { return YES; } return NO; } - (BOOL)sendCustomKeyboardEvent:(NSEvent *)event toObject:(nullable id)object { if (object == nil) { return NO; } if ([object respondsToSelector:@selector(performedCustomKeyboardEvent:)]) { if ([(id)object performedCustomKeyboardEvent:event]) { return YES; } } return NO; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Controllers/TXGlobalModels.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "TLOLocalization.h" #import "TXGlobalModels.h" NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Time NSString * _Nullable TXFormattedTimestamp(NSDate *date, NSString *format) { NSCParameterAssert(date != nil); NSCParameterAssert(format != nil); time_t global = (time_t)date.timeIntervalSince1970; const size_t outputBufferSize = 256; char outputBuffer[(outputBufferSize + 1)]; struct tm *localTime = localtime(&global); if (strftime(outputBuffer, outputBufferSize, format.UTF8String, localTime) == 0) { return nil; } return @(outputBuffer); } NSString * _Nullable TXHumanReadableTimeInterval(NSTimeInterval dateInterval, BOOL shortValue, NSCalendarUnit orderMatrix) { /* Default what we will return */ if (orderMatrix == 0) { orderMatrix = (NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond); } /* Convert calendar units to a text rep */ NSMutableArray *units = [NSMutableArray arrayWithCapacity:6]; if (orderMatrix & NSCalendarUnitYear) { [units addObject:@(NSCalendarUnitYear)]; } if (orderMatrix & NSCalendarUnitMonth) { [units addObject:@(NSCalendarUnitMonth)]; } if (orderMatrix & NSCalendarUnitDay) { [units addObject:@(NSCalendarUnitDay)]; } if (orderMatrix & NSCalendarUnitHour) { [units addObject:@(NSCalendarUnitHour)]; } if (orderMatrix & NSCalendarUnitMinute) { [units addObject:@(NSCalendarUnitMinute)]; } if (orderMatrix & NSCalendarUnitSecond) { [units addObject:@(NSCalendarUnitSecond)]; } /* Build compare information */ NSCalendar *systemCalendar = [NSCalendar currentCalendar]; NSDate *date1 = [NSDate date]; NSDate *date2 = [NSDate dateWithTimeIntervalSinceNow:dateInterval]; /* Perform comparison */ NSDateComponents *breakdownInfo = [systemCalendar components:orderMatrix fromDate:date1 toDate:date2 options:0]; if (breakdownInfo == nil) { return nil; } NSMutableString *returnResult = nil; for (NSNumber *unit in units) { NSInteger unitValue = [breakdownInfo valueForComponent:unit.unsignedIntegerValue]; /* If results isn't zero, we show it */ if (unitValue == 0) { continue; } if (unitValue < 0) { unitValue *= (-1); } NSString *languageKey = nil; if (unitValue == 1) { // plurals languageKey = [NSString stringWithFormat:@"fko-64-%@", unit]; } else { languageKey = [NSString stringWithFormat:@"eoq-pr-%@", unit]; } /* shortValue returns only the first time component */ if (shortValue) { return [NSString stringWithFormat:@"%ld %@", unitValue, TXTLS(languageKey)]; } if (returnResult == nil) { returnResult = [NSMutableString string]; } if (unit == units.lastObject) { [returnResult appendFormat:@"%ld %@", unitValue, TXTLS(languageKey)]; } else { [returnResult appendFormat:@"%ld %@, ", unitValue, TXTLS(languageKey)]; } } if (returnResult.length > 0) { return [returnResult copy]; } /* Return "0 seconds" when there are no results. */ return [NSString stringWithFormat:@"0 %@", TXTLS(@"BasicLanguage[eoq-pr-128]")]; } NSString * _Nullable TXFormatDateLongStyle(id dateObject, BOOL relativeOutput) { return TXFormatDate(dateObject, NSDateFormatterLongStyle, NSDateFormatterLongStyle, relativeOutput); } NSString * _Nullable TXFormatDate(id dateObject, NSDateFormatterStyle dateStyle, NSDateFormatterStyle timeStyle, BOOL relativeOutput) { NSCParameterAssert(dateObject != nil); NSDateFormatter *dateFormatter = [NSDateFormatter new]; dateFormatter.doesRelativeDateFormatting = relativeOutput; dateFormatter.lenient = YES; dateFormatter.dateStyle = dateStyle; dateFormatter.timeStyle = timeStyle; NSString *resultString = nil; if ([dateObject isKindOfClass:[NSString class]]) { resultString = [dateFormatter stringForObjectValue:dateObject]; } else if ([dateObject isKindOfClass:[NSDate class]]) { resultString = [dateFormatter stringFromDate:dateObject]; } return resultString; } NSDateFormatter *TXSharedISOStandardDateFormatter(void) { static NSDateFormatter *_isoStandardDateFormatter = nil; if (_isoStandardDateFormatter == nil) { NSDateFormatter *dateFormatter = [NSDateFormatter new]; dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; //2011-10-19T16:40:51.620Z _isoStandardDateFormatter = dateFormatter; } return _isoStandardDateFormatter; } #pragma mark - #pragma mark Misc NSUInteger TXRandomNumber(u_int32_t maximum) { return arc4random_uniform(maximum); } NSString *TXFormattedNumber(NSInteger number) { return [NSNumberFormatter localizedStringFromNumber:@(number) numberStyle:NSNumberFormatterDecimalStyle]; } NSComparator NSDefaultComparator = ^(id object1, id object2) { return [object1 compare:object2]; }; NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Controllers/TXMasterController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "BuildConfig.h" #import "NSObjectHelperPrivate.h" #import "OELReachability.h" #import "TDCAlert.h" #import "TLOEncryptionManagerPrivate.h" #import "TLOLicenseManagerPrivate.h" #import "TLOLocalization.h" #import "TLOSpeechSynthesizerPrivate.h" #import "THOPluginManagerPrivate.h" #import "TDCLicenseManagerDialogPrivate.h" #import "TVCLogControllerHistoricLogFilePrivate.h" #import "TVCLogControllerInlineMediaServicePrivate.h" #import "TVCLogControllerOperationQueuePrivate.h" #import "TVCMainWindowPrivate.h" #import "IRCChannelPrivate.h" #import "IRCChannelMemberListPrivate.h" #import "IRCCommandIndexPrivate.h" #import "IRCExtrasPrivate.h" #import "IRCWorldPrivate.h" #import "TPCApplicationInfoPrivate.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TPCResourceManagerPrivate.h" #import "TPCSandboxMigrationPrivate.h" #import "TPCThemeControllerPrivate.h" #import "TXMenuControllerPrivate.h" #import "TXWindowControllerPrivate.h" #import "TXMasterControllerPrivate.h" #import "IRCClient.h" #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 #import #endif NS_ASSUME_NONNULL_BEGIN @interface TXMasterController () @property (nonatomic, strong, readwrite) IRCWorld *world; @property (nonatomic, assign, readwrite) BOOL debugModeIsOn; @property (nonatomic, assign, readwrite) BOOL ghostModeIsOn; @property (nonatomic, assign, readwrite) BOOL applicationIsActive; @property (nonatomic, assign, readwrite) BOOL applicationIsLaunched; @property (nonatomic, assign, readwrite) BOOL applicationIsTerminating; @property (nonatomic, assign, readwrite) BOOL applicationIsChangingActiveState; @property (readonly) BOOL isSafeToPerformApplicationTermination; @property (nonatomic, assign, readwrite) BOOL terminateHistoricLogSaveFinished; @property (nonatomic, strong, readwrite) IBOutlet TVCMainWindow *mainWindow; @property (nonatomic, weak, readwrite) IBOutlet TXMenuController *menuController; @property (nonatomic, assign) NSUInteger applicationLaunchRemainder; #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 @property (nonatomic, strong, readwrite) SPUStandardUpdaterController *updateController; #endif @end @implementation TXMasterController #pragma mark - #pragma mark Initialization - (instancetype)init { if ((self = [super init])) { [NSObject setGlobalMasterControllerClassReference:self]; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { LogToConsoleSetDefaultSubsystemToMainBundle(@"General"); NSUInteger keyboardKeys = ([NSEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask); if ((keyboardKeys & NSEventModifierFlagControl) == NSEventModifierFlagControl) { self.debugModeIsOn = YES; LogToConsoleInfo("Launching in debug mode"); } #if defined(DEBUG) self.ghostModeIsOn = YES; // Do not use auto connect during debug #else if ((keyboardKeys & NSEventModifierFlagShift) == NSEventModifierFlagShift) { self.ghostModeIsOn = YES; LogToConsoleInfo("Launching without auto connecting to the configured servers"); } #endif } - (void)awakeFromNib { static BOOL _awakeFromNibCalled = NO; if (_awakeFromNibCalled == NO) { _awakeFromNibCalled = YES; [self _awakeFromNib]; } } - (void)_awakeFromNib { /* Migrate files and preferences */ [TPCSandboxMigration migrateResources]; /* Initialize preferences */ [TPCPreferences initPreferences]; /* Call shared instance to warm it */ [TXSharedApplication sharedAppearance]; /* We wait until -awakeFromNib to wake the window so that the menu controller created by the main nib has time to load. */ [RZMainBundle() loadNibNamed:@"TVCMainWindow" owner:self topLevelObjects:nil]; } - (void)applicationWakeStepOne { self.world = [IRCWorld new]; } - (void)applicationWakeStepTwo { [IRCCommandIndex populateCommandIndex]; [self prepareNetworkReachabilityNotifier]; [RZWorkspaceNotificationCenter() addObserver:self selector:@selector(computerDidWakeUp:) name:NSWorkspaceDidWakeNotification object:nil]; [RZWorkspaceNotificationCenter() addObserver:self selector:@selector(computerWillSleep:) name:NSWorkspaceWillSleepNotification object:nil]; [RZWorkspaceNotificationCenter() addObserver:self selector:@selector(computerWillPowerOff:) name:NSWorkspaceWillPowerOffNotification object:nil]; [RZWorkspaceNotificationCenter() addObserver:self selector:@selector(computerScreenDidWake:) name:NSWorkspaceScreensDidWakeNotification object:nil]; [RZWorkspaceNotificationCenter() addObserver:self selector:@selector(computerScreenWillSleep:) name:NSWorkspaceScreensDidSleepNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(pluginsFinishedLoading:) name:THOPluginManagerFinishedLoadingPluginsNotification object:nil]; [RZAppleEventManager() setEventHandler:self andSelector:@selector(handleURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; [NSColorPanel setPickerMask:(NSColorPanelRGBModeMask | NSColorPanelGrayModeMask | NSColorPanelColorListModeMask | NSColorPanelWheelModeMask | NSColorPanelCrayonModeMask)]; [[NSColorPanel sharedColorPanel] setShowsAlpha:YES]; XRPerformBlockAsynchronouslyOnGlobalQueueWithPriority(^{ [TPCResourceManager copyResourcesToApplicationSupportFolder]; }, DISPATCH_QUEUE_PRIORITY_BACKGROUND); /* We want to guarantee some specific things happen before the app is considered "launched" and ready to use. This property counts down once each task completes and once it reaches 0, then the app is considered launched. */ /* 1 is default value because we want plugins to be loaded before we are finished launching. */ [self addObserver:self forKeyPath:@"applicationLaunchRemainder" options:NSKeyValueObservingOptionNew context:NULL]; self.applicationLaunchRemainder = 1; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 [self prepareLicenseManager]; #endif [self prepareThirdPartyServices]; /* Load plugins last so that -applicationDidFinishLaunching is posted only once they have loaded and everything else has been setup. */ [sharedPluginManager() loadPlugins]; } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:@"applicationLaunchRemainder"]) { if (self.applicationLaunchRemainder == 0) { [self applicationDidFinishLaunching]; } } } - (void)pluginsFinishedLoading:(NSNotification *)notification { self.applicationLaunchRemainder -= 1; } #pragma mark - #pragma mark Services - (void)prepareThirdPartyServiceSparkleFramework { #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 SPUStandardUpdaterController *controller = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:NO updaterDelegate:(id )self userDriverDelegate:nil]; self.updateController = controller; SPUUpdater *updater = controller.updater; if ([updater respondsToSelector:@selector(clearFeedURLFromUserDefaults)]) { [updater performSelector:@selector(clearFeedURLFromUserDefaults)]; } else { [RZUserDefaults() removeObjectForKey:@"SUFeedURL"]; } NSError *error; (void)[updater startUpdater:&error]; if (error) { LogToConsoleError("Sparkle failed to start updater: %{public}@", error.description); } #endif } - (void)prepareThirdPartyServices { [self prepareThirdPartyServiceSparkleFramework]; } - (void)prepareNetworkReachabilityNotifier { OELReachability *notifier = [TXSharedApplication sharedNetworkReachabilityNotifier]; notifier.reachableBlock = ^(OELReachability *reachability) { [self.world noteReachabilityChanged:YES]; }; notifier.unreachableBlock = ^(OELReachability *reachability) { [self.world noteReachabilityChanged:NO]; }; [notifier startNotifier]; } #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 - (void)prepareLicenseManager { TLOLicenseManagerSetup(); [[TXSharedApplication sharedLicenseManagerDialog] applicationDidFinishLaunching]; } #endif #pragma mark - #pragma mark NSApplication Delegate - (void)applicationWillFinishLaunching:(NSNotification *)notification { /* UserNotifications.framework wants delegation set before app has finished launching. A simple access to the singleton will set this for us which we can just do here. */ LogToConsoleDebug("Preparing notification controller singeton: %@", sharedNotificationController().description); } - (void)applicationDidFinishLaunching { [self removeObserver:self forKeyPath:@"applicationLaunchRemainder"]; self.applicationIsLaunched = YES; if ([self.mainWindow reloadLoadingScreen]) { [self.world autoConnectAfterWakeup:NO]; } [self.mainWindow maybeToggleFullscreenAfterLaunch]; } - (void)applicationWillResignActive:(NSNotification *)notification { self.applicationIsChangingActiveState = YES; } - (void)applicationWillBecomeActive:(NSNotification *)notification { self.applicationIsChangingActiveState = YES; } - (void)applicationDidResignActive:(NSNotification *)notification { self.applicationIsActive = NO; self.applicationIsChangingActiveState = NO; } - (void)applicationDidBecomeActive:(NSNotification *)notification { self.applicationIsActive = YES; self.applicationIsChangingActiveState = NO; } - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag { if (self.applicationIsTerminating) { return NO; } [self.mainWindow makeKeyAndOrderFront:nil]; return YES; } - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender { if (self.applicationIsTerminating) { return NO; } [self.mainWindow makeKeyAndOrderFront:nil]; return YES; } - (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)sender { /* This will have no effect on our app. Implement to suppress warning in console. */ return YES; } #pragma mark - #pragma mark NSApplication Terminate Procedure - (NSMenu *)applicationDockMenu:(NSApplication *)sender { return self.menuController.dockMenu; } - (BOOL)queryTerminate { if (self.applicationIsTerminating) { LogToConsoleTerminationProgress("Termination is already in progress"); return YES; } if ([TPCPreferences confirmQuit] == NO) { return YES; } BOOL stillConnected = NO; for (IRCClient *u in worldController().clientList) { if (u.isConnecting || u.isConnected) { stillConnected = YES; } } if (stillConnected) { BOOL result = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[77u-vp]") title:TXTLS(@"Prompts[6vj-2p]") defaultButton:TXTLS(@"Prompts[1bf-k0]") alternateButton:TXTLS(@"Prompts[qso-2g]")]; LogToConsoleTerminationProgress("Perform termination: %{BOOL}d", result); return result; } return YES; } - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { if ([self queryTerminate] == NO) { return NSTerminateCancel; } XRPerformBlockAsynchronouslyOnMainQueue(^{ [self performApplicationTerminationStepOne]; }); return NSTerminateLater; } - (BOOL)isSafeToPerformApplicationTermination { /* Clients are still disconnecting */ BOOL condition1 = (self.terminatingClientCount == 0); /* Core Data is saving */ BOOL condition2 = (TVCLogControllerHistoricLogSharedInstance().isSaving == NO && self.terminateHistoricLogSaveFinished); LogToConsoleTerminationProgress("Conditions: %{BOOL}d %{BOOL}d", condition1, condition2); return (condition1 && condition2); } - (void)performApplicationTerminationStepOne { LogToConsoleTerminationProgress("Step one entry"); self.applicationIsTerminating = YES; [[TXSharedApplication sharedAppearance] prepareForApplicationTermination]; [self.mainWindow prepareForApplicationTermination]; LogToConsoleTerminationProgress("Giving up shared application delegation"); [[NSApplication sharedApplication] setDelegate:nil]; LogToConsoleTerminationProgress("Removing workspace notification center observer"); [RZWorkspaceNotificationCenter() removeObserver:self]; LogToConsoleTerminationProgress("Removing shared notification center observer"); [RZNotificationCenter() removeObserver:self]; LogToConsoleTerminationProgress("Removing AppleScript event observer"); [RZAppleEventManager() removeEventHandlerForEventClass:kInternetEventClass andEventID:kAEGetURL]; LogToConsoleTerminationProgress("Stopping reachability notifier"); [[TXSharedApplication sharedNetworkReachabilityNotifier] stopNotifier]; LogToConsoleTerminationProgress("Stopping speech synthesizer"); [[TXSharedApplication sharedSpeechSynthesizer] setIsStopped:YES]; [TVCLogControllerInlineMediaSharedInstance() prepareForApplicationTermination]; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 [sharedEncryptionManager() prepareForApplicationTermination]; #endif [self.menuController prepareForApplicationTermination]; [self performApplicationTerminationStepTwo]; } - (void)performApplicationTerminationStepTwo { if (self.applicationIsTerminating == NO) { return; } LogToConsoleTerminationProgress("Step two entry"); self.terminatingClientCount = worldController().clientCount; [self.world prepareForApplicationTermination]; if (self.isSafeToPerformApplicationTermination) { [self performApplicationTerminationStepThree]; return; } /* We want certain things to 100% happen before the app completely closes. This block that is performed below loops until all these actions are completed. Notable actions: gracefully leaving IRC, saving historic logs, etc. */ XRPerformBlockAsynchronouslyOnGlobalQueueWithPriority(^{ do { /* We wait until this value reaches zero so that view controllers had the chance to perform any changes they want to historic log. */ if (self.terminatingClientCount == 0) { [TVCLogControllerHistoricLogSharedInstance() prepareForApplicationTermination]; self.terminateHistoricLogSaveFinished = YES; } /* Sleep a little bit so we aren't looping a lot. */ [NSThread sleepForTimeInterval:0.5]; } while (self.isSafeToPerformApplicationTermination == NO); XRPerformBlockAsynchronouslyOnMainQueue(^{ [self performApplicationTerminationStepThree]; }); }, DISPATCH_QUEUE_PRIORITY_HIGH); } - (void)performApplicationTerminationStepThree { if (self.applicationIsTerminating == NO) { return; } LogToConsoleTerminationProgress("Step three entry"); if (self.skipTerminateSave == NO) { LogToConsoleTerminationProgress("Saving IRC world"); [self.world save]; } LogToConsoleTerminationProgress("Suspending member list dispatch queue"); [IRCChannelMemberList suspendMemberListSerialQueues]; LogToConsoleTerminationProgress("Unloading plugins"); [sharedPluginManager() unloadPlugins]; [windowController() prepareForApplicationTermination]; [themeController() prepareForApplicationTermination]; LogToConsoleTerminationProgress("Saving running internal"); [TPCApplicationInfo saveTimeIntervalSinceApplicationInstall]; LogToConsoleTerminationProgress("Terminate"); [NSApp replyToApplicationShouldTerminate:YES]; } - (void)terminateGracefully { self.applicationIsTerminating = YES; [RZSharedApplication() terminate:nil]; } #pragma mark - #pragma mark NSWorkspace Notifications - (void)handleURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSAppleEventDescriptor *description = [event descriptorAtIndex:1]; NSString *stringValue = description.stringValue; [IRCExtras parseIRCProtocolURI:stringValue withDescriptor:event]; } - (void)computerScreenWillSleep:(NSNotification *)note { LogToConsole("Preparing for screen sleep"); [self.world prepareForScreenSleep]; } - (void)computerScreenDidWake:(NSNotification *)note { LogToConsole("Waking from screen sleep"); [self.world wakeFromScreenSleep]; } - (void)computerWillSleep:(NSNotification *)note { LogToConsole("Preparing for sleep"); [self.world prepareForSleep]; [[TXSharedApplication sharedSpeechSynthesizer] setIsStopped:YES]; [[TXSharedApplication sharedSpeechSynthesizer] clearQueue]; [[TXSharedApplication sharedNetworkReachabilityNotifier] stopNotifier]; } - (void)computerDidWakeUp:(NSNotification *)note { LogToConsole("Waking from sleep"); [[TXSharedApplication sharedSpeechSynthesizer] setIsStopped:NO]; [[TXSharedApplication sharedNetworkReachabilityNotifier] startNotifier]; [self.world autoConnectAfterWakeup:YES]; } - (void)computerWillPowerOff:(NSNotification *)note { [self terminateGracefully]; } #pragma mark - #pragma mark Sparkle Delegate #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 - (void)updaterWillRelaunchApplication:(SPUUpdater *)updater { self.applicationIsTerminating = YES; } - (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)updater { BOOL receiveBetaUpdates = [TPCPreferences receiveBetaUpdates]; if (receiveBetaUpdates) { return [NSSet setWithObject:@"beta"]; } return [NSSet set]; } #endif @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Controllers/TXMenuController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCClientConfig.h" #import "IRCClientPrivate.h" #import "IRCChannelPrivate.h" #import "IRCChannelMode.h" #import "IRCChannelUser.h" #import "IRCExtrasPrivate.h" #import "IRCISupportInfo.h" #import "IRCUser.h" #import "IRCWorldPrivate.h" #import "TVCBasicTableView.h" #import "TVCLogController.h" #import "TVCLogViewPrivate.h" #import "TVCLogViewInternalWK2.h" #import "TVCMemberList.h" #import "TVCMainWindowPrivate.h" #import "TVCMainWindowSplitView.h" #import "TVCMainWindowTextView.h" #import "TLOEncryptionManagerPrivate.h" #import "TLOLicenseManagerPrivate.h" #import "TLOLocalization.h" #import "TLOpenLink.h" #import "TDCAboutDialogPrivate.h" #import "TDCAlert.h" #import "TDCChannelInviteSheetPrivate.h" #import "TDCChannelModifyModesSheetPrivate.h" #import "TDCChannelModifyTopicSheetPrivate.h" #import "TDCChannelPropertiesSheetPrivate.h" #import "TDCChannelSpotlightControllerPrivate.h" #import "TDCFileTransferDialogPrivate.h" #import "TDCInputPrompt.h" #import "TDCLicenseManagerDialogPrivate.h" #import "TDCNicknameColorSheetPrivate.h" #import "TDCPreferencesControllerPrivate.h" #import "TDCServerChangeNicknameSheetPrivate.h" #import "TDCServerHighlightListSheetPrivate.h" #import "TDCServerPropertiesSheetPrivate.h" #import "TDCWelcomeSheetPrivate.h" #import "TPCPathInfoPrivate.h" #import "TPCPreferencesImportExport.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesReload.h" #import "TPCPreferencesUserDefaults.h" #import "TXMasterControllerPrivate.h" #import "TXWindowControllerPrivate.h" #import "TXMenuControllerPrivate.h" #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 #import #endif NS_ASSUME_NONNULL_BEGIN #define _popWindowViewIfExists(c) if ([windowController() maybeBringWindowForward:(c)]) { \ return; \ } @interface TXMenuController () @property (nonatomic, assign) BOOL menuIsOpen; @property (nonatomic, assign) BOOL menuPerformedActionLastOpen; @property (nonatomic, weak) IRCClient *pointedClient; @property (nonatomic, weak) IRCChannel *pointedChannel; @property (nonatomic, copy) NSString *currentSearchPhrase; @property (readonly, nullable) TVCLogController *selectedViewController; @property (readonly, nullable) TVCLogView *selectedViewControllerBackingView; @property (readonly) TDCFileTransferDialog *fileTransferController; @property (nonatomic, strong, readwrite) IBOutlet NSMenu *channelViewChannelNameMenu; @property (nonatomic, strong, readwrite) IBOutlet NSMenu *channelViewGeneralMenu; @property (nonatomic, strong, readwrite) IBOutlet NSMenu *channelViewURLMenu; @property (nonatomic, strong, readwrite) IBOutlet NSMenu *dockMenu; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 @property (nonatomic, strong, readwrite) IBOutlet NSMenu *encryptionManagerStatusMenu; #endif @property (nonatomic, weak, readwrite) IBOutlet NSMenu *mainMenuNavigationChannelListMenu; @property (nonatomic, weak, readwrite) IBOutlet NSMenu *mainMenuChannelMenu; @property (nonatomic, weak, readwrite) IBOutlet NSMenu *mainMenuQueryMenu; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *mainMenuChannelMenuItem; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *mainMenuQueryMenuItem; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *mainMenuServerMenuItem; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *mainMenuWindowMenuItem; @property (nonatomic, strong, readwrite) IBOutlet NSMenu *mainWindowSegmentedControllerCellMenu; @property (nonatomic, strong, readwrite) IBOutlet NSMenu *serverListNoSelectionMenu; @property (nonatomic, strong, readwrite) IBOutlet NSMenu *userControlMenu; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *muteNotificationsDockMenuItem; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *muteNotificationsFileMenuItem; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *muteNotificationsSoundsDockMenuItem; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *muteNotificationsSoundsFileMenuItem; @end @implementation TXMenuController - (void)prepareInitialState { self.currentSearchPhrase = @""; if ([TPCPreferences soundIsMuted]) { self.muteNotificationsSoundsDockMenuItem.state = NSControlStateValueOn; self.muteNotificationsSoundsFileMenuItem.state = NSControlStateValueOn; } [self.channelViewGeneralMenu itemWithTag:MTWKGeneralChannelMenu].submenu = [self.mainMenuChannelMenu copy]; [self setupOtherServices]; [RZNotificationCenter() addObserver:self selector:@selector(menuItemWillPerformedAction:) name:NSMenuWillSendActionNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(menuItemPerformedAction:) name:NSMenuDidSendActionNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(mainWindowSelectionChanged:) name:TVCMainWindowSelectionChangedNotification object:nil]; } - (void)setupOtherServices { [self.fileTransferController startUsingDownloadDestinationURL]; } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Preparing menu controller"); [self.fileTransferController prepareForApplicationTermination]; } - (void)preferencesChanged { [self.fileTransferController clearIPAddress]; } - (void)mainWindowSelectionChanged:(NSNotification *)notification { if (self.menuIsOpen == NO) { [self resetSelectedItems]; } /* When the selection changes, menus that may be dynamic are force revalidated so that Command I (or other shortcuts) work with channel selected, but not for the server console. */ NSMenuItem *channelMenu = self.mainMenuChannelMenuItem; [self _forceMenuItemValidation:channelMenu]; NSMenuItem *queryMenu = self.mainMenuQueryMenuItem; [self _forceMenuItemValidation:queryMenu]; } - (void)_forceMenuValidation:(NSMenu *)menu { NSParameterAssert(menu != nil); for (NSMenuItem *menuItem in menu.itemArray) { [self _forceMenuItemValidation:menuItem]; } } - (void)_forceMenuItemValidation:(NSMenuItem *)menuItem { NSParameterAssert(menuItem != nil); id target = menuItem.target; if (target == nil) { return; } if ([target respondsToSelector:@selector(validateMenuItem:)]) { [target performSelector:@selector(validateMenuItem:) withObject:menuItem]; } } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { NSParameterAssert(menuItem != nil); if (masterController().applicationIsTerminating) { return NO; } /* Menu validation works in two passes: 1. First -_validateMenuItem: is called which performs validation for the individual menu item including hiding it and related items. 2. The result is then passed to the logic below which performs more specialized work such as disabling large group of menu items when the trial has expired. */ BOOL validationResult = [self _validateMenuItem:menuItem]; if (validationResult == NO) { return NO; } NSUInteger tag = menuItem.tag; /* The submenus of the main menu are all targets of the menu controller so that we can chose to hide or show some depending on context. For the top most submenus, we have nothing further to do after performing initial validation. */ switch (tag) { case MTMainMenuApp: case MTMainMenuFile: case MTMainMenuEdit: case MTMainMenuView: case MTMainMenuServer: case MTMainMenuChannel: case MTMainMenuQuery: case MTMainMenuNavigate: case MTMainMenuWindow: case MTMainMenuHelp: { return YES; } } // switch /* When the main window is not the focused window or when we are in a sheet, most items can be disabled which means at this point we will default to disabled and allow the bottom logic to enable only the bare essentials. */ BOOL defaultToNoForSheet = ( mainWindow().attachedSheet != nil || (mainWindow().mainWindow == NO && mainWindow().isBeneathMouse == NO)); if (defaultToNoForSheet) { validationResult = NO; } /* If the app has not finished launching, then default everything to disabled. */ if (masterController().applicationIsLaunched == NO) { validationResult = NO; } /* If trial is expired, then default everything to disabled. */ BOOL isTrialExpired = NO; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 if (TLOLicenseManagerTextualIsRegistered() == NO && TLOLicenseManagerIsTrialExpired()) { /* Set flag letting logic know trial expired */ isTrialExpired = YES; /* Disable everything by default except "Manage license…" */ validationResult = (tag == MTMMAppManageLicense); } // if #endif /* If certain items are hidden because of sheet but not because of the trial being expired, then enable additional items. */ if (validationResult == NO && defaultToNoForSheet && isTrialExpired == NO) { switch (tag) { case MTMMAppAboutApp: // "About Textual" case MTMMAppPreferences: // "Preferences…" case MTMMAppManageLicense: // "Manage license…" case MTMMAppCheckForUpdates: // "Check for updates…" case MTMMHelpAdvancedMenuEnableDeveloperMode: // "Enable Developer Mode" case MTMMHelpAdvancedMenuHiddenPreferences: // "Hidden Preferences…" case MTMMFileDisableAllNotifications: // "Disable All Notifications" case MTMMFileDisableAllNotificationSounds: // "Disable All Notification Sounds" case MTDockMenuDisableAllNotifications: // "Disable All Notifications" case MTDockMenuDisableAllNotificationSounds: // "Disable All Notification Sounds" { validationResult = YES; break; } } // switch } // if /* These are the bare minimum of menu items that must be enabled at all times because they are essential to the entire application. */ /* This list may look incomplete but it isn't. Many menu items, such as Undo, Cut, Copy, Quit, etc. are not a target of the menu controller which means they never pass through this logic. */ if (validationResult == NO) { switch (tag) { case MTMMAppAboutApp: // "About Textual" case MTMMAppQuitApp: // "Quit Textual & IRC" case MTMMFilePrint: // "Print" case MTMMFileCloseWindow: // "Close Window" case MTMMEditPaste: // "Paste" case MTMMViewToggleFullscreen: // "Toggle Fullscreen" case MTMMWindowMainWindow: // "Main Window" case MTMMHelpAcknowledgements: // "Acknowledgements" case MTMMHelpLicenseAgreement: // "License Agreement" case MTMMHelpPrivacyPolicy: // "Privacy Policy" case MTMMHelpFrequentlyAskedQuestions: // "Frequently Asked Questions" case MTMMHelpKnowledgeBaseMenu: // "Knowledge Base" case MTMMHelpAdvancedMenu: // "Advanced" case MTMMHelpAdvancedMenuExportPreferences: // "Export Preferences" { validationResult = YES; break; } default: { if (menuItem.parentItem.tag == MTMMHelpKnowledgeBaseMenu) { validationResult = YES; } break; } } // switch } // if return validationResult; } - (BOOL)_validateMenuItem:(NSMenuItem *)menuItem { NSParameterAssert(menuItem != nil); NSUInteger tag = menuItem.tag; IRCClient *u = mainWindow().selectedClient; IRCChannel *c = mainWindow().selectedChannel; switch (tag) { case MTMainMenuChannel: // "Channel" { BOOL isChannel = c.isChannel; menuItem.hidden = (isChannel == NO); if (isChannel) { menuItem.submenu = self.mainMenuChannelMenu; } else { menuItem.submenu = nil; } return YES; } case MTMainMenuQuery: // "Query" { BOOL isQuery = (c.isPrivateMessage || c.isUtility); menuItem.hidden = (isQuery == NO); if (isQuery) { menuItem.submenu = self.mainMenuQueryMenu; } else { menuItem.submenu = nil; } return YES; } case MTMMAppManageLicense: // "Manage license…" { #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 0 menuItem.hidden = YES; #endif return YES; } case MTMMAppCheckForUpdates: // "Check for Updates" { #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 0 menuItem.hidden = YES; #endif return YES; } case MTMMFileCloseWindow: // "Close Window" { TXCommandWKeyAction keyAction = [TPCPreferences commandWKeyAction]; if (keyAction == TXCommandWKeyActionCloseWindow || mainWindow().keyWindow == NO) { menuItem.title = TXTLS(@"BasicLanguage[1f6-bg]"); return YES; } if (u == nil) { return NO; } switch (keyAction) { case TXCommandWKeyActionPartChannel: { if (c == nil) { menuItem.title = TXTLS(@"BasicLanguage[1f6-bg]"); return NO; } if (c.isChannel) { menuItem.title = TXTLS(@"BasicLanguage[5td-3f]"); if (c.isActive == NO) { return NO; } } else if (c.isPrivateMessage) { menuItem.title = TXTLS(@"BasicLanguage[hri-l0]"); } else if (c.isUtility) { menuItem.title = TXTLS(@"BasicLanguage[hri-l0]"); } break; } case TXCommandWKeyActionDisconnect: { menuItem.title = TXTLS(@"BasicLanguage[w3a-je]", u.networkNameAlt); if (u.isConnecting == NO && u.isConnected == NO) { return NO; } break; } case TXCommandWKeyActionTerminate: { menuItem.title = TXTLS(@"BasicLanguage[x97-ro]"); break; } default: { break; } } return YES; } case MTMMEditPaste: // "Paste" case MTWKGeneralPaste: // "Paste" (WebView) { NSString *currentPasteboard = RZPasteboard().stringContent; if (currentPasteboard.length == 0) { return NO; } if (mainWindow().keyWindow) { return mainWindowTextField().editable; } id firstResponder = [NSApp keyWindow].firstResponder; if ([firstResponder respondsToSelector:@selector(isEditable)]) { return [firstResponder isEditable]; } return NO; } case MTMMViewMarkScrollback: // "Mark Scrollback" case MTMMViewScrollbackMarker: // "Scrollback Marker" case MTMMViewMarkAllAsRead: // "Mark All as Read" case MTMMViewClearScrollback: // "Clear Scrollback" case MTMMViewIncreaseFontSize: // "Increase Font Size" case MTMMViewDecreaseFontSize: // "Decrease Font Size" case MTMMNavigationJumpToCurrentSession: // "Jump to Current Session" case MTMMNavigationJumpToPresent: // "Jump to Present" { return (self.selectedViewController != nil); } case MTMMViewToggleFullscreen: { NSWindowCollectionBehavior collectionBehavior = [NSApp keyWindow].collectionBehavior; return ((collectionBehavior & NSWindowCollectionBehaviorFullScreenAuxiliary) == NSWindowCollectionBehaviorFullScreenAuxiliary || (collectionBehavior & NSWindowCollectionBehaviorFullScreenPrimary) == NSWindowCollectionBehaviorFullScreenPrimary); } case MTMMServerConnect: // "Connect" { if (u == nil) { menuItem.hidden = NO; return NO; } BOOL connected = (u.isConnected || u.isConnecting); menuItem.hidden = connected; return (connected == NO && u.isQuitting == NO); } case MTMMServerConnectWithoutProxy: // "Connect Without Proxy" { NSUInteger flags = ([NSEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask); if (flags != NSEventModifierFlagShift) { menuItem.hidden = YES; return NO; } if (u == nil) { menuItem.hidden = YES; return NO; } BOOL condition = (u.isConnected || u.isConnecting || u.config.proxyType == IRCConnectionProxyTypeNone); menuItem.hidden = condition; return (condition == NO && u.isQuitting == NO); } case MTMMServerDisconnect: // "Disconnect" { BOOL connected = (u.isConnected || u.isConnecting); menuItem.hidden = (connected == NO); return connected; } case MTMMServerCancelReconnect: // "Cancel Reconnect" { BOOL reconnecting = u.isReconnecting; menuItem.hidden = (reconnecting == NO); return reconnecting; } case MTMMServerChannelList: // "Channel List…" { return u.isLoggedIn; } case MTMMServerChangeNickname: // "Change Nickname…" case MTWKGeneralChangeNickname: // "Change Nickname…" { return u.isConnected; } case MTMMServerDuplicateServer: // "Duplicate Server" case MTMMServerAddChannel: // "Add Channel…" case MTMMServerServerProperties: // "Server Properties…" { return (u != nil); } case MTMMServerDeleteServer: // "Delete Server…" { return (u && u.isConnecting == NO && u.isConnected == NO); } case MTMMNavigationNextHighlight: // "Next Highlight" case MTMMNavigationPreviousHighlight: // "Previous Highlight" { TVCLogController *viewController = self.selectedViewController; if (viewController == nil) { return NO; } return [viewController highlightAvailable:(tag == MTMMNavigationPreviousHighlight)]; } case MTMMChannelJoinChannel: // "Join Channel" { menuItem.hidden = (u.isLoggedIn == NO || c.isActive); return YES; } case MTMMChannelLeaveChannel: // "Leave Channel" { menuItem.hidden = (u.isLoggedIn == NO || c.isActive == NO); NSMenuItem *joinChannel = [menuItem.menu itemWithTag:MTMMChannelJoinChannel]; [menuItem.menu itemWithTag:MTMMChannelLeaveChannelSeparator].hidden = (menuItem.hidden && joinChannel.hidden); return YES; } case MTMMChannelAddChannel: // "Add Channel…" { return (u != nil); } case MTMMChannelViewLogs: // "View Logs" { return [TPCPreferences logToDiskIsEnabled]; } case MTMMChannelModifyTopic: // "Modify Topic" case MTMMChannelModesMenu: // "Modes" case MTMMChannelListOfBans: // "List of Bans" { return (u.isLoggedIn && c.isActive); } case MTMMChannelListOfBanExceptions: // "List of Ban Exceptions" { menuItem.hidden = ([u.supportInfo isListSupported:IRCISupportInfoListTypeBanException] == NO); return (u.isLoggedIn && c.isActive); } case MTMMChannelListOfInviteExceptions: // "List of Invite Exceptions" { menuItem.hidden = ([u.supportInfo isListSupported:IRCISupportInfoListTypeInviteException] == NO); return (u.isLoggedIn && c.isActive); } case MTMMChannelListOfQuiets: // "List of Quiets" { menuItem.hidden = ([u.supportInfo isListSupported:IRCISupportInfoListTypeQuiet] == NO); return (u.isLoggedIn && c.isActive); } case MTMMQueryQueryLogs: // "Query Logs" { /* Query menu is used for utility windows too so we hide "Query Logs" for anything except private messages. */ BOOL isQuery = c.isPrivateMessage; menuItem.hidden = (isQuery == NO); [menuItem.menu itemWithTag:MTMMQueryCloseQuerySeparator].hidden = (isQuery == NO); return [TPCPreferences logToDiskIsEnabled]; } case MTMMWindowToggleVisibilityOfServerList: // "Toggle Visibility of Server List" case MTMMWindowSortChannelList: // "Sort Channel List" case MTMMWindowCenterWindow: // "Center Window" case MTMMWindowResetWindowToDefaultSize: // "Reset Window to Default Size" { BOOL isMainWindowMain = mainWindow().mainWindow; menuItem.hidden = (isMainWindowMain == NO); if (tag == MTMMWindowSortChannelList) { [menuItem.menu itemWithTag:MTMMWindowSortChannelListSeparator].hidden = (isMainWindowMain == NO); } else if (tag == MTMMWindowResetWindowToDefaultSize) { [menuItem.menu itemWithTag:MTMMWindowResetWindowToDefaultSizeSeparator].hidden = (isMainWindowMain == NO); } return YES; } case MTMMWindowMainWindow: // "Main Window" { BOOL isMainWindowMain = mainWindow().mainWindow; BOOL isMainWindowDisabled = mainWindow().disabled; menuItem.hidden = isMainWindowMain; return (isMainWindowDisabled == NO); } case MTMMWindowToggleVisibilityOfMemberList: // "Toggle Visibility of Member List" { BOOL isMainWindowMain = mainWindow().mainWindow; menuItem.hidden = (isMainWindowMain == NO); return c.isChannel; } case MTMMWindowToggleWindowAppearance: // "Toggle Window Appearance" { BOOL isMainWindowMain = mainWindow().mainWindow; menuItem.hidden = (isMainWindowMain == NO); [menuItem.menu itemWithTag:MTMMWindowToggleWindowAppearanceSeparator].hidden = (isMainWindowMain == NO); return YES; } case MTMMWindowAddressBook: // "Address Book" case MTMMWindowIgnoreList: // "Ignore List" { BOOL isMainWindowMain = mainWindow().mainWindow; menuItem.hidden = (isMainWindowMain == NO); return (u != nil); } case MTMMWindowViewLogs: // "View Logs" { return [TPCPreferences logToDiskIsEnabled]; } case MTMMWindowHighlightList: // "Highlight List" { BOOL isMainWindowMain = mainWindow().mainWindow; menuItem.hidden = (isMainWindowMain == NO); if (u == nil) { return NO; } return [TPCPreferences logHighlights]; } case MTUserControlsAddIgnore: // "Add Ignore" { /* To make it as efficient as possible, we only check for ignore for the "Add Ignore" menu item. When that menu item is validated, we validate "Modify Ignore" and "Remove Ignore" at the same time. */ NSMenuItem *modifyIgnoreMenuItem = [menuItem.menu itemWithTag:MTUserControlsModifyIgnore]; NSMenuItem *removeIgnoreMenuItem = [menuItem.menu itemWithTag:MTUserControlsRemoveIgnore]; if (c.isUtility) { modifyIgnoreMenuItem.hidden = YES; removeIgnoreMenuItem.hidden = YES; menuItem.hidden = NO; return NO; } /* If less than or more than one user is selected, then hide all menu items except "Add Ignore" and disable the "Add Ignore" item. */ NSArray *nicknames = [self selectedMembers:menuItem]; NSString *hostmask = nicknames.firstObject.user.hostmask; if (nicknames.count != 1 || hostmask == nil) { modifyIgnoreMenuItem.hidden = YES; removeIgnoreMenuItem.hidden = YES; menuItem.hidden = NO; return NO; } /* Update visibility depending on whether ignore is available */ /* When this logic was first introduced, we kept a reference to the ignores in the represented object of the menu item. This was stopped because information about the ignore can change while the menu item is still open, making the object we will reference when action is performed garbage. */ NSArray *userIgnores = [u findIgnoresForHostmask:hostmask]; BOOL condition = (userIgnores.count == 0); modifyIgnoreMenuItem.hidden = condition; removeIgnoreMenuItem.hidden = condition; menuItem.hidden = (condition == NO); return YES; } case MTUserControlsModifyIgnore: // "Modify Ignore" case MTUserControlsRemoveIgnore: // "Remove Ignore" { return YES; } case MTUserControlsInviteTo: // "Invite To…" { if (u.isLoggedIn == NO || c.isUtility) { return NO; } NSUInteger channelCount = 0; for (IRCChannel *e in u.channelList) { if (c != e && e.isChannel) { channelCount++; } } return (channelCount > 0); } case MTUserControlsGetInfo: // "Get Info (Whois)" case MTUserControlsClientToClientMenu: // "Client-to-Client" { return (u.isLoggedIn && c.isUtility == NO); } case MTUserControlsPrivateMessage: // "Private Message (Query)" { menuItem.hidden = (c.isChannel == NO); return (u.isLoggedIn && c.isUtility == NO); } case MTUserControlsGiveOp: // "Give Op (+o)" case MTUserControlsGiveHalfop: // "Give Halfop (+h)" case MTUserControlsGiveVoice: // "Give Voice (+v)" case MTUserControlsTakeOp: // "Take Op (-o)" case MTUserControlsTakeHalfop: // "Take Halfop (-h)" case MTUserControlsTakeVoice: // "Take Voice (-v)" { return (u.isLoggedIn && c.isActive); } case MTUserControlsAllModesGiven: // "All Modes Given" { return NO; } case MTUserControlsAllModesTaken: // "All Modes Taken" { #define _setHidden(tag, value) [menuItem.menu itemWithTag:(tag)].hidden = (value) if (c.isChannel == NO) { _setHidden(MTUserControlsGiveOp, YES); _setHidden(MTUserControlsGiveHalfop, YES); _setHidden(MTUserControlsGiveVoice, YES); _setHidden(MTUserControlsTakeOp, YES); _setHidden(MTUserControlsTakeHalfop, YES); _setHidden(MTUserControlsTakeVoice, YES); _setHidden(MTUserControlsAllModesGiven, YES); _setHidden(MTUserControlsAllModesGivenSeparator, YES); _setHidden(MTUserControlsAllModesTaken, YES); _setHidden(MTUserControlsAllModesTakenSeparator, YES); return NO; } _setHidden(MTUserControlsAllModesGivenSeparator, NO); _setHidden(MTUserControlsAllModesTakenSeparator, NO); NSArray *nicknames = [self selectedMembers:menuItem]; if (nicknames.count == 1) { IRCChannelUser *user = nicknames[0]; IRCUserRank userRanks = user.ranks; BOOL UserHasModeO = ((userRanks & IRCUserRankNormalOperator) == IRCUserRankNormalOperator); BOOL UserHasModeH = NO; BOOL UserHasModeV = ((userRanks & IRCUserRankVoiced) == IRCUserRankVoiced); _setHidden(MTUserControlsGiveOp, UserHasModeO); _setHidden(MTUserControlsGiveVoice, UserHasModeV); _setHidden(MTUserControlsTakeOp, (UserHasModeO == NO)); _setHidden(MTUserControlsTakeVoice, (UserHasModeV == NO)); BOOL halfOpModeSupported = [u.supportInfo modeSymbolIsUserPrefix:@"h"]; if (halfOpModeSupported == NO) { _setHidden(MTUserControlsGiveHalfop, YES); _setHidden(MTUserControlsTakeHalfop, YES); } else { UserHasModeH = ((userRanks & IRCUserRankHalfOperator) == IRCUserRankHalfOperator); _setHidden(MTUserControlsGiveHalfop, UserHasModeH); _setHidden(MTUserControlsTakeHalfop, (UserHasModeH == NO)); } BOOL hideGiveSepItem = ((UserHasModeO == NO || UserHasModeV == NO) || (UserHasModeH == NO && halfOpModeSupported)); _setHidden(MTUserControlsAllModesGiven, hideGiveSepItem); BOOL hideTakenSepItem = (UserHasModeO || UserHasModeH || UserHasModeV); _setHidden(MTUserControlsAllModesTaken, hideTakenSepItem); } else { _setHidden(MTUserControlsGiveOp, NO); _setHidden(MTUserControlsGiveHalfop, NO); _setHidden(MTUserControlsGiveVoice, NO); _setHidden(MTUserControlsTakeOp, NO); _setHidden(MTUserControlsTakeHalfop, NO); _setHidden(MTUserControlsTakeVoice, NO); _setHidden(MTUserControlsAllModesGiven, YES); _setHidden(MTUserControlsAllModesTaken, YES); } return NO; #undef _setHidden } case MTUserControlsBan: // "Ban" case MTUserControlsKick: // "Kick" case MTUserControlsBanAndKick: // "Ban and Kick" { BOOL isChannel = c.isChannel; menuItem.hidden = (isChannel == NO); [menuItem.menu itemWithTag:MTUserControlsBanAndKickSeparator].hidden = (isChannel == NO); return (u.isLoggedIn && isChannel && c.isActive); } case MTUserControlsIRCOperatorMenu: // "IRC Operator" { menuItem.hidden = (u.userIsIRCop == NO); return (u.isLoggedIn && c.isUtility == NO); } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 case MTOTRStatusButtonStartPrivateConversation: case MTOTRStatusButtonRefreshPrivateConversation: case MTOTRStatusButtonEndPrivateConversation: case MTOTRStatusButtonAuthenticateChatPartner: case MTOTRStatusButtonViewListOfFingerprints: { /* Even if we are not logged in, we still ask the encryption manager to validate the menu item first so that it can hide specific menu items. After it has done that, then we can disable if not logged in. */ if ([TPCPreferences textEncryptionIsEnabled] == NO) { return NO; } if (u.isLoggedIn == NO) { return NO; } BOOL valid = [sharedEncryptionManager() validateMenuItem:menuItem withStateOf:[u encryptionAccountNameForUser:c.name] from:[u encryptionAccountNameForLocalUser]]; return valid; } #endif case MTWKGeneralSearchWithGoogle: // "Search With Google" { TVCLogView *webView = self.selectedViewControllerBackingView; if (webView == nil) { return NO; } NSString *searchProviderName = [self searchProviderName]; menuItem.title = TXTLS(@"BasicLanguage[1ll-h9]", searchProviderName); return webView.hasSelection; } case MTWKGeneralLookUpInDictionary: // "Look Up in Dictionary" { TVCLogView *webView = self.selectedViewControllerBackingView; if (webView == nil) { return NO; } NSString *selection = webView.selection; NSUInteger selectionLength = selection.length; if (selectionLength == 0 || selectionLength > 40) { menuItem.title = TXTLS(@"BasicLanguage[o5l-4s]"); return NO; } if (selectionLength > 25) { selection = [selection substringToIndex:24]; selection = [NSString stringWithFormat:@"%@…", selection.trim]; } menuItem.title = TXTLS(@"BasicLanguage[zxs-yy]", selection); return (selectionLength > 0); } case MTWKGeneralCopy: // "Copy" (WebView) { TVCLogView *webView = self.selectedViewControllerBackingView; if (webView == nil) { return NO; } return webView.hasSelection; } case MTWKGeneralQueryLogs: // "Query Logs" (WebKit) { menuItem.hidden = (c.isPrivateMessage == NO); return [TPCPreferences logToDiskIsEnabled]; } case MTWKGeneralChannelMenu: // "Channel" (WebKit) { menuItem.hidden = (c.isChannel == NO); /* "Query Logs" will appear above this menu item, but if this is neither channel or query, then we have to hide the separator above that so it's not just sitting there with nothing beneath it. */ NSMenuItem *queryLogs = [menuItem.menu itemWithTag:MTWKGeneralQueryLogs]; [menuItem.menu itemWithTag:MTWKGeneralPasteSeparator].hidden = (menuItem.hidden && queryLogs.hidden); return YES; } case MTMMHelpAdvancedMenuEnableDeveloperMode: // Developer Mode { if ([TPCPreferences developerModeEnabled]) { menuItem.state = NSControlStateValueOn; } else { menuItem.state = NSControlStateValueOff; } return YES; } case MTMainWindowSegmentedControllerAddChannel: // "Add Channel…" { return (u != nil); } default: { break; } } return YES; } - (void)menuWillOpen:(NSMenu *)menu { self.menuIsOpen = YES; self.pointedClient = mainWindow().selectedClient; self.pointedChannel = mainWindow().selectedChannel; self.menuPerformedActionLastOpen = NO; } - (void)menuDidClose:(NSMenu *)menu { self.menuIsOpen = NO; /* This delegate callback is received before -menuItemPerformedAction: is called. So that our selected items can be reset if the user did not perform an action, we call -menuClosedTimer the next time the main queue comes around. The action is performed on the current pass which means this prevents a race. */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [self _menuClosedTimer]; }); } - (void)_menuClosedTimer { if (self.menuPerformedActionLastOpen) { return; } [self resetSelectedItems]; } - (void)menuItemWillPerformedAction:(NSNotification *)aNote { NSMenuItem *menuItem = aNote.userInfo[@"MenuItem"]; if (menuItem.target != self) { return; } self.menuPerformedActionLastOpen = YES; } - (void)menuItemPerformedAction:(NSNotification *)aNote { NSMenuItem *menuItem = aNote.userInfo[@"MenuItem"]; if (menuItem.target != self) { return; } [self resetSelectedItems]; } #pragma mark - #pragma mark Selection - (void)resetSelectedItems { self.pointedClient = nil; self.pointedChannel = nil; } - (nullable IRCClient *)selectedClient { IRCClient *pointedClient = self.pointedClient; if (pointedClient) { return pointedClient; } return mainWindow().selectedClient; } - (nullable IRCChannel *)selectedChannel { IRCChannel *pointedChannel = self.pointedChannel; if (pointedChannel) { return pointedChannel; } return mainWindow().selectedChannel; } - (nullable TVCLogController *)selectedViewController { IRCChannel *selectedChannel = self.selectedChannel; if (selectedChannel) { return selectedChannel.viewController; } return self.selectedClient.viewController; } - (nullable TVCLogView *)selectedViewControllerBackingView { TVCLogController *viewController = self.selectedViewController; return viewController.backingView; } #pragma mark - #pragma mark Selected User(s) - (BOOL)checkSelectedMembers:(id)sender { return ([self selectedMembers:sender].count > 0); } - (NSArray *)selectedMembers:(id)sender { return [self selectedMembers:sender returnStrings:NO]; } - (NSArray *)selectedMembersNicknames:(id)sender { return [self selectedMembers:sender returnStrings:YES]; } - (NSArray *)selectedMembers:(id)sender returnStrings:(BOOL)returnStrings { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isActive == NO) { return @[]; } /* Return a specific nickname for WebView events */ NSString *pointedNickname = nil; if ([sender isKindOfClass:[NSMenuItem class]]) { pointedNickname = ((NSMenuItem *)sender).userInfo; } else { pointedNickname = self.pointedNickname; } if (pointedNickname) { if (returnStrings) { return @[pointedNickname]; } IRCChannelUser *user = [c findMember:pointedNickname]; if (user) { return @[user]; } return @[]; } /* If we did not have a specific nickname, then query the user list for selected rows. */ NSMutableArray *userArray = [NSMutableArray array]; NSIndexSet *selectedRows = mainWindowMemberList().selectedRowIndexes; [selectedRows enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { IRCChannelUser *user = [mainWindowMemberList() itemAtRow:index]; if (returnStrings) { [userArray addObject:user.user.nickname]; } else { [userArray addObject:user]; } }]; return [userArray copy]; } - (void)deselectMembers:(id)sender { if ([sender isKindOfClass:[NSMenuItem class]]) { if (((NSMenuItem *)sender).userInfo.length > 0) { return; // Nothing to deselect when our sender used userInfo } } if (self.pointedNickname) { self.pointedNickname = nil; return; } [mainWindowMemberList() deselectAll:sender]; } #pragma mark - #pragma mark Find Panel - (void)_showFindPromptOpenDialog:(id)sender { void (^promptCompletionBlock)(NSString *) = ^(NSString *resultString) { if ([self.currentSearchPhrase isEqualToString:resultString]) { return; } self.currentSearchPhrase = resultString; TVCLogView *webView = self.selectedViewControllerBackingView; [webView findString:resultString movingForward:YES]; }; NSString *resultString = nil; TVCAlertResponseButton response = [TDCInputPrompt promptWithMessage:TXTLS(@"Prompts[d2w-4o]") title:TXTLS(@"Prompts[akr-eh]") defaultButton:TXTLS(@"Prompts[q5h-xx]") alternateButton:TXTLS(@"Prompts[qso-2g]") prefillString:self.currentSearchPhrase resultString:&resultString]; if (response == TVCAlertResponseButtonFirst) { promptCompletionBlock(resultString); } } - (void)showFindPrompt:(id)sender { NSParameterAssert(sender != nil); if (mainWindow().keyWindow == NO) { return; } if ([sender tag] == MTMMEditFindMenuFind || self.currentSearchPhrase.length == 0) { [self _showFindPromptOpenDialog:sender]; return; } TVCLogView *webView = self.selectedViewControllerBackingView; if ([sender tag] == MTMMEditFindMenuFindNext) { [webView findString:self.currentSearchPhrase movingForward:YES]; } else { [webView findString:self.currentSearchPhrase movingForward:NO]; } } #pragma mark - #pragma mark Edit - (void)copy:(id)sender { id firstResponder = [NSApp keyWindow].firstResponder; if ([firstResponder respondsToSelector:@selector(copy:)]) { [firstResponder performSelector:@selector(copy:) withObject:sender]; } } - (void)paste:(id)sender { if (mainWindow().keyWindow) { [mainWindowTextField() focus]; [mainWindowTextField() paste:sender]; return; } id firstResponder = [NSApp keyWindow].firstResponder; if ([firstResponder respondsToSelector:@selector(paste:)]) { [firstResponder performSelector:@selector(paste:) withObject:sender]; } } - (void)print:(id)sender { if (mainWindow().keyWindow) { TVCLogView *webView = self.selectedViewControllerBackingView; if (webView == nil) { return; } [webView print]; return; } id firstResponder = [NSApp keyWindow].firstResponder; if ([firstResponder respondsToSelector:@selector(print:)]) { [firstResponder performSelector:@selector(print:) withObject:sender]; } } #pragma mark - #pragma mark Backing View - (void)copyLogAsHtml:(id)sender { TVCLogView *webView = self.selectedViewControllerBackingView; if (webView == nil) { return; } [webView copyContentString]; } - (void)openWebInspector:(id)sender { /* When WebKit2 first announced, there was no way for an app to take an NSMenu, modify it, and return the result when a context menu is presented. WebKit1 had a delegate method for that named: -webView:contextMenuItemsForElement:defaultMenuItems: WebKit2 added this delegate method, in private, but it was not available until the next update of macOS. 'Til then, we had to fake "Inspect Element" because we did not have access to the default implementation that is available through the delegate method. Apple now flags the function names that were used to do that. This will break "Inspect Element" on one version of macOS. Which one I don't even recall. But it is worth it to make the review process smoother. Sorry. */ (void) [TDCAlert alertWithMessage:TXTLS(@"Prompts[kig-m1]") title:TXTLS(@"Prompts[ujw-64]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } - (void)markScrollback:(id)sender { TVCLogController *viewController = self.selectedViewController; if (viewController == nil) { return; } [viewController mark]; } - (void)gotoScrollbackMarker:(id)sender { TVCLogController *viewController = self.selectedViewController; if (viewController == nil) { return; } [viewController goToMark]; } - (void)clearScrollback:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil) { return; } if (c) { [mainWindow() clearContentsOfChannel:c]; } else { [mainWindow() clearContentsOfClient:u]; } } - (void)increaseLogFontSize:(id)sender { [mainWindow() changeTextSize:YES]; } - (void)decreaseLogFontSize:(id)sender { [mainWindow() changeTextSize:NO]; } - (NSString *)searchProviderName { NSDictionary *preferredWebServices = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"NSPreferredWebServices"]; NSDictionary *defaultSearchProvider = [preferredWebServices dictionaryForKey:@"NSWebServicesProviderWebSearch"]; NSString *searchProviderName = [defaultSearchProvider stringForKey:@"NSDefaultDisplayName"]; if (searchProviderName == nil) { return @"Google"; } return searchProviderName; } - (void)searchGoogle:(id)sender { TVCLogView *webView = self.selectedViewControllerBackingView; if (webView == nil) { return; } NSString *selection = webView.selection; if (selection.length == 0) { return; } NSPasteboard *searchPasteboard = [NSPasteboard pasteboardWithUniqueName]; searchPasteboard.stringContent = selection; NSPerformService(@"Search With %WebSearchProvider@", searchPasteboard); } - (void)lookUpInDictionary:(id)sender { TVCLogView *webView = self.selectedViewControllerBackingView; if (webView == nil) { return; } NSString *selection = webView.selection; if (selection.length == 0) { return; } NSString *urlString = [NSString stringWithFormat:@"dict://%@", selection.percentEncodedString]; [TLOpenLink openWithString:urlString]; } #pragma mark - #pragma mark Server - (void)connect:(id)sender { IRCClient *u = self.selectedClient; if (u == nil || u.isConnecting || u.isConnected || u.isQuitting) { return; } [u connect]; [mainWindow() expandClient:u]; } - (void)connectBypassingProxy:(id)sender { IRCClient *u = self.selectedClient; if (u == nil || u.isConnecting || u.isConnected || u.isQuitting) { return; } [u connect:IRCClientConnectModeNormal bypassProxy:YES]; [mainWindow() expandClient:u]; } - (void)disconnect:(id)sender { IRCClient *u = self.selectedClient; if (u == nil || (u.isConnecting == NO && u.isConnected == NO) || u.isQuitting) { return; } [u quit]; } - (void)cancelReconnection:(id)sender { IRCClient *u = self.selectedClient; if (u == nil) { return; } [u cancelReconnect]; } - (void)showServerChannelList:(id)sender { IRCClient *u = self.selectedClient; if (u == nil || u.isLoggedIn == NO) { return; } [u createChannelListDialog]; [u requestChannelList]; } - (void)addServer:(id)sender { [windowController() popMainWindowSheetIfExists]; TDCServerPropertiesSheet *sheet = [[TDCServerPropertiesSheet alloc] initWithClient:nil]; sheet.delegate = self; sheet.window = mainWindow(); [sheet startWithSelection:TDCServerPropertiesSheetSelectionDefault context:nil]; [windowController() addWindowToWindowList:sheet]; } - (void)duplicateServer:(id)sender { IRCClient *u = self.selectedClient; if (u == nil) { return; } IRCClientConfigMutable *config = [u.config uniqueCopyMutable]; config.connectionName = [config.connectionName stringByAppendingString:@"_"]; IRCClient *newClient = [worldController() createClientWithConfig:config reload:YES]; if (newClient.config.sidebarItemExpanded) { // Only expand new client if old was expanded already. [mainWindow() expandClient:newClient]; } [worldController() save]; } - (void)deleteServer:(id)sender { IRCClient *u = self.selectedClient; if (u == nil || u.isConnecting || u.isConnected) { return; } BOOL result = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[etl-ss]") title:TXTLS(@"Prompts[0kz-wd]") defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]")]; if (result == NO) { return; } [worldController() destroyClient:u]; [worldController() save]; } #pragma mark - #pragma mark Channel - (void)joinChannel:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO || c.isActive) { return; } [u joinChannel:c]; [mainWindow() select:c]; } - (void)leaveChannel:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; /* Second boxed in condition is because this is used for queries too */ if (u == nil || c == nil || (c.isChannel && (u.isLoggedIn == NO || c.isActive == NO))) { return; } if (c.isChannel) { [u partChannel:c]; return; } [worldController() destroyChannel:c]; } - (void)addChannel:(id)sender { [windowController() popMainWindowSheetIfExists]; IRCClient *u = self.selectedClient; if (u == nil) { return; } TDCChannelPropertiesSheet *sheet = [[TDCChannelPropertiesSheet alloc] initWithClient:u]; sheet.delegate = self; sheet.window = mainWindow(); [sheet start]; [windowController() addWindowToWindowList:sheet]; } - (void)deleteChannel:(id)sender { IRCChannel *c = self.selectedChannel; if (c == nil) { return; } if (c.isChannel) { BOOL result = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[516-ms]") title:TXTLS(@"Prompts[i8o-7z]") defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]") suppressionKey:@"delete_channel" suppressionText:nil]; if (result == NO) { return; } } [worldController() destroyChannel:c]; [worldController() save]; } - (void)copyUniqueIdentifier:(id)sender { IRCChannel *c = self.selectedChannel; if (c == nil) { return; } [RZPasteboard() setStringContent:c.uniqueIdentifier]; } #pragma mark - #pragma mark Other Actions - (void)copyUrl:(id)sender { NSString *pointedUrl = ((NSMenuItem *)sender).userInfo; if (pointedUrl.length == 0) { return; } RZPasteboard().stringContent = pointedUrl; } - (void)joinChannelClicked:(id)sender { NSParameterAssert(sender != nil); IRCClient *u = self.selectedClient; if (u == nil || u.isLoggedIn == NO) { return; } NSString *pointedChannelName = nil; if ([sender isKindOfClass:[NSMenuItem class]]) { pointedChannelName = ((NSMenuItem *)sender).userInfo; } else if ([sender isKindOfClass:[NSString class]]) { pointedChannelName = sender; } else { return; } if ([u stringIsChannelName:pointedChannelName] == NO) { return; } IRCChannel *c = [u findChannelOrCreate:pointedChannelName]; [u joinChannel:c]; [mainWindow() select:c]; } - (void)emptyAction:(id)sender { /* Empty action used to validate submenus */ } #pragma mark - #pragma mark Ignores - (void)memberAddIgnore:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } NSArray *nicknames = [self selectedMembersNicknames:sender]; if (nicknames.count == 0) { return; } [self deselectMembers:sender]; NSString *command = [NSString stringWithFormat:@"ignore %@", nicknames[0]]; [u sendCommand:command completeTarget:YES target:c.name]; } - (void)memberRemoveIgnore:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } NSArray *nicknames = [self selectedMembersNicknames:sender]; if (nicknames.count == 0) { return; } [self deselectMembers:sender]; NSString *command = [NSString stringWithFormat:@"unignore %@", nicknames[0]]; [u sendCommand:command completeTarget:YES target:c.name]; } - (void)memberModifyIgnore:(id)sender { IRCClient *u = self.selectedClient; if (u == nil) { return; } NSArray *nicknames = [self selectedMembers:sender]; [self deselectMembers:sender]; /* User's hostmask and other information can change between the point the menu item is opened and the point the action is performed. We therefore perform a new query for ignores when performing action. */ NSString *hostmask = nicknames.firstObject.user.hostmask; if (nicknames.count != 1 || hostmask == nil) { return; } NSArray *userIgnores = [u findIgnoresForHostmask:hostmask]; /* If we have more than one user ignore, then open the address book instead of a specific ignore. */ if (userIgnores.count == 1) { [self showServerPropertiesSheetForClient:u withSelection:TDCServerPropertiesSheetSelectionNewIgnoreEntry context:userIgnores[0]]; } else { [self showServerPropertiesSheetForClient:u withSelection:TDCServerPropertiesSheetSelectionAddressBook context:nil]; } } #pragma mark - #pragma mark Members - (void)memberInMemberListDoubleClicked:(id)sender { NSInteger rowBeneathMouse = mainWindowMemberList().rowBeneathMouse; if (rowBeneathMouse < 0) { return; } TXUserDoubleClickAction action = [TPCPreferences userDoubleClickOption]; if (action == TXUserDoubleClickActionWhois) { [self whoisSelectedMembers:sender]; } else if (action == TXUserDoubleClickActionPrivateMessage) { [self memberStartPrivateMessage:sender]; } else if (action == TXUserDoubleClickActionInsertTextField) { [self memberInsertNameIntoTextField:sender]; } } - (void)memberInChannelViewDoubleClicked:(id)sender { TXUserDoubleClickAction action = [TPCPreferences userDoubleClickOption]; if (action == TXUserDoubleClickActionWhois) { [self whoisSelectedMembers:sender]; } else if (action == TXUserDoubleClickActionPrivateMessage) { [self memberStartPrivateMessage:sender]; } else if (action == TXUserDoubleClickActionInsertTextField) { [self memberInsertNameIntoTextField:sender]; } } - (void)memberInsertNameIntoTextField:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } NSArray *nicknames = [self selectedMembersNicknames:sender]; if (nicknames.count == 0) { return; } [self deselectMembers:sender]; TVCMainWindowTextView *textView = mainWindowTextField(); NSRange selectedRange = textView.selectedRange; NSMutableString *stringToInsert = [NSMutableString string]; if (selectedRange.location > 0) { UniChar previousCharacter = [textView.stringValue characterAtIndex:(selectedRange.location - 1)]; if ([[NSCharacterSet whitespaceCharacterSet] characterIsMember:previousCharacter] == NO) { [stringToInsert appendString:@" "]; } } NSString *nicknamesString = [nicknames componentsJoinedByString:@", "]; [stringToInsert appendString:nicknamesString]; NSString *completionSuffix = [TPCPreferences tabCompletionSuffix]; if (completionSuffix != nil) { [stringToInsert appendString:completionSuffix]; } [textView replaceCharactersInRange:selectedRange withString:stringToInsert]; [textView resetFontColorInRange:selectedRange]; [textView focus]; } - (void)memberSendWhois:(id)sender { [self whoisSelectedMembers:sender]; } - (void)whoisSelectedMembers:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { [u sendWhois:nickname]; } [self deselectMembers:sender]; } - (void)memberStartPrivateMessage:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { IRCChannel *query = [u findChannelOrCreate:nickname isPrivateMessage:YES]; [mainWindow() select:query]; } [self deselectMembers:sender]; } - (void)memberSendCTCPPing:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { [u sendCTCPPing:nickname]; } [self deselectMembers:sender]; } - (void)memberSendCTCPFinger:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { [u sendCTCPQuery:nickname command:@"FINGER" text:nil]; } [self deselectMembers:sender]; } - (void)memberSendCTCPTime:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { [u sendCTCPQuery:nickname command:@"TIME" text:nil]; } [self deselectMembers:sender]; } - (void)memberSendCTCPVersion:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { [u sendCTCPQuery:nickname command:@"VERSION" text:nil]; } [self deselectMembers:sender]; } - (void)memberSendCTCPUserinfo:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { [u sendCTCPQuery:nickname command:@"USERINFO" text:nil]; } [self deselectMembers:sender]; } - (void)memberSendCTCPClientInfo:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { [u sendCTCPQuery:nickname command:@"CLIENTINFO" text:nil]; } [self deselectMembers:sender]; } - (void)_processModeChange:(id)sender usingCommand:(NSString *)modeCommand { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } NSArray *nicknames = [self selectedMembersNicknames:sender]; [self deselectMembers:sender]; NSString *nicknamesString = [nicknames componentsJoinedByString:@" "]; NSString *command = [NSString stringWithFormat:@"%@ %@", modeCommand, nicknamesString]; [u sendCommand:command completeTarget:YES target:c.name]; } - (void)memberModeGiveOp:(id)sender { [self _processModeChange:sender usingCommand:@"OP"]; } - (void)memberModeTakeOp:(id)sender { [self _processModeChange:sender usingCommand:@"DEOP"]; } - (void)memberModeGiveHalfop:(id)sender { [self _processModeChange:sender usingCommand:@"HALFOP"]; } - (void)memberModeTakeHalfop:(id)sender { [self _processModeChange:sender usingCommand:@"DEHALFOP"]; } - (void)memberModeGiveVoice:(id)sender { [self _processModeChange:sender usingCommand:@"VOICE"]; } - (void)memberModeTakeVoice:(id)sender { [self _processModeChange:sender usingCommand:@"DEVOICE"]; } - (void)memberKickFromChannel:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { [u kick:nickname inChannel:c]; } [self deselectMembers:sender]; } - (void)memberBanFromChannel:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { NSString *command = [NSString stringWithFormat:@"BAN %@", nickname]; [u sendCommand:command completeTarget:YES target:c.name]; } [self deselectMembers:sender]; } - (void)memberKickbanFromChannel:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { NSString *command = [NSString stringWithFormat:@"KICKBAN %@ %@", nickname, [TPCPreferences defaultKickMessage]]; [u sendCommand:command completeTarget:YES target:c.name]; } [self deselectMembers:sender]; } - (void)memberKillFromServer:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { NSString *command = [NSString stringWithFormat:@"KILL %@ %@", nickname, [TPCPreferences IRCopDefaultKillMessage]]; [u sendCommand:command]; } [self deselectMembers:sender]; } - (void)memberBanFromServer:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { if ([u nicknameIsMyself:nickname]) { [u printDebugInformation:TXTLS(@"IRC[0r1-5l]", u.serverAddress) inChannel:c]; continue; } NSString *command = [NSString stringWithFormat:@"GLINE %@ %@", nickname, [TPCPreferences IRCopDefaultGlineMessage]]; [u sendCommand:command]; } [self deselectMembers:sender]; } - (void)memberShunOnServer:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO) { return; } for (NSString *nickname in [self selectedMembersNicknames:sender]) { NSString *command = [NSString stringWithFormat:@"SHUN %@ %@", nickname, [TPCPreferences IRCopDefaultShunMessage]]; [u sendCommand:command]; } [self deselectMembers:sender]; } - (void)_showSetVhostPromptOpenDialog:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO) { return; } NSArray *nicknames = [self selectedMembersNicknames:sender]; if (nicknames.count == 0) { return; } [self deselectMembers:sender]; void (^promptCompletionBlock)(NSString *) = ^(NSString *resultString) { NSString *vhost = resultString.trimAndGetFirstToken; if (vhost.length == 0) { return; } for (NSString *nickname in nicknames) { NSString *command = [NSString stringWithFormat:@"hs setall %@ %@", nickname, vhost]; [u sendCommand:command completeTarget:NO target:nil]; } }; NSString *vhost = nil; TVCAlertResponseButton response = [TDCInputPrompt promptWithMessage:TXTLS(@"Prompts[2mx-jf]") title:TXTLS(@"Prompts[7gr-e4]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:TXTLS(@"Prompts[qso-2g]") prefillString:nil resultString:&vhost]; if (response == TVCAlertResponseButtonFirst) { promptCompletionBlock(vhost); } } - (void)showSetVhostPrompt:(id)sender { [self _showSetVhostPromptOpenDialog:sender]; } #pragma mark - #pragma mark File Transfers - (TDCFileTransferDialog *)fileTransferController { return [TXSharedApplication sharedFileTransferDialog]; } - (void)showFileTransfersWindow:(id)sender { [self.fileTransferController show:YES restorePosition:YES]; } - (void)memberSendFileRequest:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO) { return; } NSArray *nicknames = [self selectedMembersNicknames:sender]; if (nicknames.count == 0) { return; } [self deselectMembers:sender]; NSOpenPanel *openPanel = [NSOpenPanel openPanel]; openPanel.allowsMultipleSelection = YES; openPanel.canChooseDirectories = NO; openPanel.canChooseFiles = YES; openPanel.canCreateDirectories = NO; openPanel.resolvesAliases = YES; [openPanel beginSheetModalForWindow:mainWindow() completionHandler:^(NSInteger returnCode) { if (returnCode != NSModalResponseOK) { return; } [self.fileTransferController.fileTransferTable beginUpdates]; for (NSString *nickname in nicknames) { for (NSURL *path in openPanel.URLs) { [self.fileTransferController addSenderForClient:u nickname:nickname path:path.path autoOpen:YES]; } } [self.fileTransferController.fileTransferTable endUpdates]; }]; } - (void)memberSendDroppedFilesToSelectedChannel:(NSArray *)files { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) { return; } [self memberSendDroppedFiles:files to:c.name]; } - (void)memberSendDroppedFiles:(NSArray *)files row:(NSUInteger)row { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) { return; } IRCChannelUser *member = [mainWindowMemberList() itemAtRow:row]; [self memberSendDroppedFiles:files to:member.user.nickname]; } - (void)memberSendDroppedFiles:(NSArray *)files to:(NSString *)nickname { NSParameterAssert(files != nil); NSParameterAssert(nickname != nil); IRCClient *u = self.selectedClient; if (u == nil || u.isLoggedIn == NO) { return; } [self.fileTransferController.fileTransferTable beginUpdates]; for (NSString *file in files) { BOOL isDirectory = NO; if ([RZFileManager() fileExistsAtPath:file isDirectory:&isDirectory] == NO) { continue; } else if (isDirectory) { continue; } [self.fileTransferController addSenderForClient:u nickname:nickname path:file autoOpen:YES]; } [self.fileTransferController.fileTransferTable endUpdates]; } #pragma mark - #pragma mark Logging - (void)openLogLocation:(id)sender { NSURL *path = [TPCPathInfo transcriptFolderURL]; if (path == nil) { return; } if ([RZFileManager() fileExistsAtURL:path]) { [RZWorkspace() openURL:path]; return; } [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[f05-hu]") title:TXTLS(@"Prompts[k55-19]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } - (void)openChannelLogs:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil) { return; } NSURL *path = c.logFilePath; if (path == nil) { return; } if ([RZFileManager() fileExistsAtURL:path]) { [RZWorkspace() openURL:path]; return; } [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[f05-hu]") title:TXTLS(@"Prompts[k55-19]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } #pragma mark - #pragma mark Help - (void)openAcknowledgements:(id)sender { NSURL *Acknowledgements = [RZMainBundle() URLForResource:@"Acknowledgements" withExtension:@"pdf" subdirectory:@"Documentation"]; [RZWorkspace() openURL:Acknowledgements]; } - (void)openHelpMenuItem:(id)sender { NSParameterAssert(sender != nil); NSDictionary *_helpMenuLinks = @{ @(MTMMHelpLicenseAgreement) : @"https://help.codeux.com/textual/End-User-License-Agreement.kb", @(MTMMHelpPrivacyPolicy) : @"https://help.codeux.com/textual/Privacy-Policy.kb", @(MTMMHelpFrequentlyAskedQuestions) : @"https://help.codeux.com/textual/Frequently-Asked-Questions.kb", @(MTMMHelpKBMenuKnowledgeBaseHome) : @"https://help.codeux.com/textual/home.kb", @(MTMMHelpKBMenuChatEncryption) : @"https://help.codeux.com/textual/Off-the-Record-Messaging.kb", @(MTMMHelpKBMenuCommandReference) : @"https://help.codeux.com/textual/Command-Reference.kb", @(MTMMHelpKBMenuFeatureRequests) : @"https://help.codeux.com/textual/Support.kb", @(MTMMHelpKBMenuKeyboardShortcuts) : @"https://help.codeux.com/textual/Keyboard-Shortcuts.kb", @(MTMMHelpKBMenuMemoryManagement) : @"https://help.codeux.com/textual/Memory-Management.kb", @(MTMMHelpKBMenuNetworkTimeouts) : @"https://help.codeux.com/textual/Network-Timeouts.kb", @(MTMMHelpKBMenuTextFormatting) : @"https://help.codeux.com/textual/Text-Formatting.kb", @(MTMMHelpKBMenuStylingInformation) : @"https://help.codeux.com/textual/Styles.kb", @(MTMMHelpKBMenuConnectingWithCertificate) : @"https://help.codeux.com/textual/Using-CertFP.kb", @(MTMMHelpKBMenuConnectingToBouncer) : @"https://help.codeux.com/textual/Connecting-to-ZNC-Bouncer.kb", @(MTMMHelpKBMenuDCCFileTransferInformation) : @"https://help.codeux.com/textual/DCC-File-Transfer-Information.kb" }; NSString *link = _helpMenuLinks[@([sender tag])]; [TLOpenLink openWithString:link inBackground:NO]; } - (void)openStandaloneStoreWebpage:(id)sender { [TLOpenLink openWithString:@"https://www.textualapp.com/standalone-store" inBackground:NO]; } - (void)contactSupport:(id)sender { [TLOpenLink openWithString:@"https://contact.codeux.com/" inBackground:NO]; } - (void)connectToTextualHelpChannel:(id)sender { [IRCExtras createConnectionToServer:@"irc.libera.chat +6697" channelList:@"#textual" connectWhenCreated:YES mergeConnectionIfPossible:YES selectFirstChannelAdded:YES]; } - (void)connectToTextualTestingChannel:(id)sender { [IRCExtras createConnectionToServer:@"irc.libera.chat +6697" channelList:@"#textual-testing" connectWhenCreated:YES mergeConnectionIfPossible:YES selectFirstChannelAdded:YES]; } #pragma mark - #pragma mark IRC - (void)showChannelBanList:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } [u createChannelBanListSheet]; [u sendModes:@"+b" withParameters:nil inChannel:c]; } - (void)showChannelBanExceptionList:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } [u createChannelBanExceptionListSheet]; [u sendModes:@"+e" withParameters:nil inChannel:c]; } - (void)showChannelInviteExceptionList:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } [u createChannelInviteExceptionListSheet]; [u sendModes:@"+I" withParameters:nil inChannel:c]; } - (void)showChannelQuietList:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } [u createChannelQuietListSheet]; [u sendModes:@"+q" withParameters:nil inChannel:c]; } - (void)toggleChannelModerationMode:(id)sender { NSParameterAssert(sender != nil); IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } NSString *modeSymbol = nil; if ([sender tag] == MTMMChannelModesMenuRemoveModerated) { modeSymbol = @"-m"; } else { modeSymbol = @"+m"; } [u sendModes:modeSymbol withParameters:nil inChannel:c]; } - (void)toggleChannelInviteMode:(id)sender { NSParameterAssert(sender != nil); IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } NSString *modeSymbol = nil; if ([sender tag] == MTMMChannelModesMenuRemoveInviteOnly) { modeSymbol = @"-i"; } else { modeSymbol = @"+i"; } [u sendModes:modeSymbol withParameters:nil inChannel:c]; } #pragma mark - #pragma mark Window - (void)closeWindow:(id)sender { TXCommandWKeyAction keyAction = [TPCPreferences commandWKeyAction]; if (keyAction == TXCommandWKeyActionCloseWindow || mainWindow().keyWindow == NO) { NSWindow *windowToClose = [NSApp keyWindow]; if (windowToClose == nil) { windowToClose = [NSApp mainWindow]; } if (windowToClose) { [windowToClose performClose:sender]; } return; } IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil) { return; } switch (keyAction) { case TXCommandWKeyActionPartChannel: { if (c == nil) { return; } if (c.isChannel) { if (c.isActive == NO) { return; } [u partChannel:c]; } else { [worldController() destroyChannel:c]; } break; } case TXCommandWKeyActionDisconnect: { if (u.isConnecting == NO && u.isConnected == NO) { return; } [u quit]; break; } case TXCommandWKeyActionTerminate: { [NSApp terminate:sender]; break; } default: { break; } } } - (void)showMainWindow:(id)sender { [mainWindow() makeKeyAndOrderFront:sender]; } - (void)centerMainWindow:(id)sender { [mainWindow() exactlyCenterWindow]; } - (void)toggleFullscreen:(id)sender { [[NSApp keyWindow] toggleFullScreen:sender]; } - (void)resetMainWindowFrame:(id)sender { if (mainWindow().inFullscreenMode) { [mainWindow() toggleFullScreen:sender]; } [mainWindow() setFrame:[mainWindow() defaultWindowFrame] display:YES animate:YES]; [mainWindow() exactlyCenterWindow]; } - (void)sortChannelListNames:(id)sender { for (IRCClient *u in worldController().clientList) { NSMutableArray *channelList = [u.channelList mutableCopy]; [channelList sortUsingComparator:^NSComparisonResult(IRCChannel *channel1, IRCChannel *channel2) { if (channel1.isChannel && channel2.isChannel == NO) { return NSOrderedAscending; } NSString *name1 = channel1.name.lowercaseString; NSString *name2 = channel2.name.lowercaseString; return [name1 compare:name2]; }]; if ([channelList isEqualToArray:u.channelList]) { continue; } u.channelList = channelList; [u reloadServerListItems]; } [worldController() save]; } - (void)markAllAsRead:(id)sender { [mainWindow() markAllAsRead]; } #pragma mark - #pragma mark Preferences - (void)importPreferences:(id)sender { [TPCPreferencesImportExport importInWindow:mainWindow()]; } - (void)exportPreferences:(id)sender { [TPCPreferencesImportExport exportInWindow:mainWindow()]; } #pragma mark - #pragma mark Off-the-Record Messaging #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 #define _encryptionNotEnabled ([TPCPreferences textEncryptionIsEnabled] == NO) - (void)encryptionStartPrivateConversation:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (_encryptionNotEnabled || u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) { return; } [sharedEncryptionManager() beginConversationWith:[u encryptionAccountNameForUser:c.name] from:[u encryptionAccountNameForLocalUser]]; } - (void)encryptionRefreshPrivateConversation:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (_encryptionNotEnabled || u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) { return; } [sharedEncryptionManager() refreshConversationWith:[u encryptionAccountNameForUser:c.name] from:[u encryptionAccountNameForLocalUser]]; } - (void)encryptionEndPrivateConversation:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (_encryptionNotEnabled || u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) { return; } [sharedEncryptionManager() endConversationWith:[u encryptionAccountNameForUser:c.name] from:[u encryptionAccountNameForLocalUser]]; } - (void)encryptionAuthenticateChatPartner:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (_encryptionNotEnabled || u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) { return; } [sharedEncryptionManager() authenticateUser:[u encryptionAccountNameForUser:c.name] from:[u encryptionAccountNameForLocalUser]]; } - (void)encryptionListFingerprints:(id)sender { [sharedEncryptionManager() presentListOfFingerprints]; } - (void)encryptionWhatIsThisInformation:(id)sender { [TLOpenLink openWithString:@"https://help.codeux.com/textual/Off-the-Record-Messaging.kb" inBackground:NO]; } #undef _encryptionNotEnabled #endif #pragma mark - #pragma mark Notifications - (void)toggleMuteOnNotificationsShortcutOn:(BOOL)toggleOn { sharedNotificationController().areNotificationsDisabled = toggleOn; NSControlStateValue state = ((toggleOn) ? NSControlStateValueOn : NSControlStateValueOff); self.muteNotificationsFileMenuItem.state = state; self.muteNotificationsDockMenuItem.state = state; } - (void)toggleMuteOnNotificationSoundsShortcutOn:(BOOL)toggleOn { [TPCPreferences setSoundIsMuted:toggleOn]; NSControlStateValue state = ((toggleOn) ? NSControlStateValueOn : NSControlStateValueOff); self.muteNotificationsSoundsDockMenuItem.state = state; self.muteNotificationsSoundsFileMenuItem.state = state; } - (void)toggleMuteOnNotificationSounds:(id)sender { if ([TPCPreferences soundIsMuted]) { [self toggleMuteOnNotificationSoundsShortcutOn:NO]; } else { [self toggleMuteOnNotificationSoundsShortcutOn:YES]; } } - (void)toggleMuteOnNotifications:(id)sender { if (sharedNotificationController().areNotificationsDisabled) { [self toggleMuteOnNotificationsShortcutOn:NO]; } else { [self toggleMuteOnNotificationsShortcutOn:YES]; } } #pragma mark - #pragma mark Appearance - (void)resetMainWindowAppearance:(id)sender { [TPCPreferences setAppearance:TXPreferredAppearanceInherited]; [TPCPreferences performReloadAction:TPCPreferencesReloadActionAppearance]; } - (void)toggleMainWindowAppearance:(id)sender { TXPreferredAppearance appearance = [TPCPreferences appearance]; switch (appearance) { case TXPreferredAppearanceInherited: { TXAppearance *appAppearance = [TXSharedApplication sharedAppearance]; if (appAppearance.properties.isDarkAppearance == NO) { appearance = TXPreferredAppearanceDark; } else { appearance = TXPreferredAppearanceLight; } break; } case TXPreferredAppearanceLight: { appearance = TXPreferredAppearanceDark; break; } case TXPreferredAppearanceDark: { appearance = TXPreferredAppearanceLight; break; } } // switch() [TPCPreferences setAppearance:appearance]; [TPCPreferences performReloadAction:TPCPreferencesReloadActionAppearance]; } - (void)toggleServerListVisibility:(id)sender { [mainWindow().contentSplitView toggleServerListVisibility]; } - (void)toggleMemberListVisibility:(id)sender { mainWindowMemberList().isHiddenByUser = (mainWindowMemberList().isHiddenByUser == NO); [mainWindow().contentSplitView toggleMemberListVisibility]; } - (void)forceReloadTheme:(id)sender { [mainWindow() reloadTheme]; } #pragma mark - #pragma mark License Manager - (void)manageLicense:(id)sender { #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 [self manageLicense:sender activateLicenseKey:nil licenseKeyPassedByArgument:NO]; #endif } #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 - (void)manageLicense:(id)sender activateLicenseKey:(nullable NSString *)licenseKey { [self manageLicense:sender activateLicenseKey:licenseKey licenseKeyPassedByArgument:NO]; } - (void)manageLicense:(id)sender activateLicenseKeyWithURL:(NSURL *)licenseKeyURL { NSParameterAssert(licenseKeyURL != nil); NSString *path = licenseKeyURL.path; if (path == nil) { return; } NSCharacterSet *slashCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"/"]; NSString *licenseKey = [path stringByTrimmingCharactersInSet:slashCharacterSet]; if (licenseKey.length == 0) { return; } [self manageLicense:sender activateLicenseKey:licenseKey licenseKeyPassedByArgument:NO]; } - (void)manageLicense:(id)sender activateLicenseKey:(nullable NSString *)licenseKey licenseKeyPassedByArgument:(BOOL)licenseKeyPassedByArgument { TDCLicenseManagerDialog *licenseDialog = [TXSharedApplication sharedLicenseManagerDialog]; [licenseDialog show]; if (licenseKey) { [licenseDialog activateLicenseKey:licenseKey silently:licenseKeyPassedByArgument]; } } #endif #pragma mark - #pragma mark Developer - (void)toggleDeveloperMode:(id)sender { [TPCPreferences setDeveloperModeEnabled:([TPCPreferences developerModeEnabled] == NO)]; [TPCPreferences performReloadAction:TPCPreferencesReloadActionIRCCommandCache]; } - (void)resetDoNotAskMePopupWarnings:(id)sender { NSDictionary *settings = [RZUserDefaults() dictionaryRepresentation]; for (NSString *key in settings) { if ([key hasPrefix:TDCAlertSuppressionPrefix] == NO) { continue; } [RZUserDefaults() setBool:NO forKey:key]; } } #pragma mark - #pragma mark Sparkle Framework - (void)checkForUpdates:(id)sender { #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 SPUStandardUpdaterController *controller = masterController().updateController; [controller checkForUpdates:sender]; #endif } #pragma mark - #pragma mark Navigation - (void)navigateToTreeItemAtURL:(NSURL *)url { NSParameterAssert(url != nil); NSString *path = url.path; if (path == nil) { return; } NSCharacterSet *slashCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"/"]; NSString *identifier = [path stringByTrimmingCharactersInSet:slashCharacterSet]; if (identifier.length == 0) { return; } [self navigateToTreeItemWithIdentifier:identifier]; } - (void)navigateToTreeItemWithIdentifier:(NSString *)identifier { NSParameterAssert(identifier != nil); /* Do not use assert for this condition so we don't crash user when we open a malformed URL. */ if (identifier.length != 36) { return; } IRCTreeItem *item = [worldController() findItemWithId:identifier]; if (item == nil) { return; } [self navigateToTreeItem:item]; } - (void)navigateToTreeItem:(IRCTreeItem *)item { NSParameterAssert(item != nil); [mainWindow() select:item]; } - (void)populateNavigationChannelList { [self.mainMenuNavigationChannelListMenu removeAllItems]; NSUInteger channelCount = 0; for (IRCClient *u in worldController().clientList) { NSMenu *channelSubmenu = [NSMenu new]; NSMenuItem *clientMenuItem = [NSMenuItem new]; clientMenuItem.title = u.name; clientMenuItem.submenu = channelSubmenu; for (IRCChannel *c in u.channelList) { NSMenuItem *channelMenuItem = nil; if (channelCount >= 10) { channelMenuItem = [NSMenuItem menuItemWithTitle:c.name target:self action:@selector(_navigateToChannelInNavigationList:)]; } else { NSUInteger keyboardIndex = (channelCount + 1); if (keyboardIndex == 10) { keyboardIndex = 0; // Have 0 as the last item. } channelMenuItem = [NSMenuItem menuItemWithTitle:c.name target:self action:@selector(_navigateToChannelInNavigationList:) keyEquivalent:[NSString stringWithUniChar:('0' + keyboardIndex)] keyEquivalentMask:NSEventModifierFlagCommand]; } channelMenuItem.userInfo = [worldController() pasteboardStringForItem:c]; [channelSubmenu addItem:channelMenuItem]; channelCount += 1; } [self.mainMenuNavigationChannelListMenu addItem:clientMenuItem]; } } - (void)_navigateToChannelInNavigationList:(NSMenuItem *)sender { IRCTreeItem *treeItem = [worldController() findItemWithPasteboardString:sender.userInfo]; if (treeItem == nil) { return; } [mainWindow() select:treeItem]; } - (void)performNavigationAction:(id)sender { NSParameterAssert(sender != nil); IRCClient *u = self.selectedClient; if (u == nil) { return; } switch ([sender tag]) { case MTMMNavigationServersMenuNextServer: { [mainWindow() selectNextServer:sender]; break; } case MTMMNavigationServersMenuPreviousServer: { [mainWindow() selectPreviousServer:sender]; break; } case MTMMNavigationServersMenuNextActiveServer: { [mainWindow() selectNextActiveServer:sender]; break; } case MTMMNavigationServersMenuPreviousActiveServer: { [mainWindow() selectPreviousActiveServer:sender]; break; } case MTMMNavigationChannelsMenuNextChannel: { [mainWindow() selectNextChannel:sender]; break; } case MTMMNavigationChannelsMenuPreviousChannel: { [mainWindow() selectPreviousChannel:sender]; break; } case MTMMNavigationChannelsMenuNextActiveChannel: { [mainWindow() selectNextActiveChannel:sender]; break; } case MTMMNavigationChannelsMenuPreviousActiveChannel: { [mainWindow() selectPreviousActiveChannel:sender]; break; } case MTMMNavigationChannelsMenuNextUnreadChannel: { [mainWindow() selectNextUnreadChannel:sender]; break; } case MTMMNavigationChannelsMenuPreviousUnreadChannel: { [mainWindow() selectPreviousUnreadChannel:sender]; break; } case MTMMNavigationMoveBackward: { [mainWindow() selectPreviousWindow:sender]; break; } case MTMMNavigationMoveForward: { [mainWindow() selectNextWindow:sender]; break; } case MTMMNavigationPreviousSelection: { [mainWindow() selectPreviousSelection:sender]; break; } } // switch() } - (void)onNextHighlight:(id)sender { TVCLogController *viewController = self.selectedViewController; if (viewController == nil) { return; } [viewController nextHighlight]; } - (void)onPreviousHighlight:(id)sender { TVCLogController *viewController = self.selectedViewController; if (viewController == nil) { return; } [viewController previousHighlight]; } - (void)jumpToCurrentSession:(id)sender { TVCLogController *viewController = self.selectedViewController; if (viewController == nil) { return; } [viewController jumpToCurrentSession]; } - (void)jumpToPresent:(id)sender { TVCLogController *viewController = self.selectedViewController; if (viewController == nil) { return; } [viewController jumpToPresent]; } #pragma mark - #pragma mark Channel Properties Sheet - (void)showChannelPropertiesSheet:(id)sender { [windowController() popMainWindowSheetIfExists]; IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || c.isChannel == NO) { return; } TDCChannelPropertiesSheet *sheet = [[TDCChannelPropertiesSheet alloc] initWithChannel:c]; sheet.delegate = self; sheet.window = mainWindow(); [sheet start]; [windowController() addWindowToWindowList:sheet]; } - (void)channelPropertiesSheet:(TDCChannelPropertiesSheet *)sender onOk:(IRCChannelConfig *)config { IRCClient *u = sender.client; if (u == nil) { return; } IRCChannel *c = sender.channel; if (c == nil) { [worldController() createChannelWithConfig:config onClient:u]; [mainWindow() expandClient:u]; return; } [c updateConfig:config]; [worldController() save]; } - (void)channelPropertiesSheetWillClose:(TDCChannelPropertiesSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Channel Invite Sheet - (void)memberSendInvite:(id)sender { [windowController() popMainWindowSheetIfExists]; IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO || c.isActive == NO) { return; } NSArray *nicknames = [self selectedMembersNicknames:sender]; if (nicknames.count == 0) { return; } [self deselectMembers:sender]; NSMutableArray *channels = [NSMutableArray array]; for (IRCChannel *e in u.channelList) { if (c != e && e.isChannel) { [channels addObject:e.name]; } } if (channels.count == 0) { return; } TDCChannelInviteSheet *sheet = [[TDCChannelInviteSheet alloc] initWithNicknames:nicknames onClient:u]; sheet.delegate = (id)self; sheet.window = mainWindow(); [sheet startWithChannels:channels]; [windowController() addWindowToWindowList:sheet]; } - (void)channelInviteSheet:(TDCChannelInviteSheet *)sender onSelectChannel:(NSString *)channelName { IRCClient *u = sender.client; if (u == nil || u.isLoggedIn == NO) { return; } for (NSString *nickname in sender.nicknames) { [u sendInviteTo:nickname toJoinChannelNamed:channelName]; } } - (void)channelInviteSheetWillClose:(TDCChannelInviteSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Address Book Sheet - (void)showAddressBook:(id)sender { IRCClient *u = self.selectedClient; if (u == nil) { return; } [self showServerPropertiesSheetForClient:u withSelection:TDCServerPropertiesSheetSelectionAddressBook context:nil]; } - (void)showIgnoreList:(id)sender { [self showAddressBook:sender]; } #pragma mark - #pragma mark Welcome Sheet - (void)showWelcomeSheet:(id)sender { [windowController() popMainWindowSheetIfExists]; TDCWelcomeSheet *sheet = [[TDCWelcomeSheet alloc] initWithWindow:mainWindow()]; sheet.delegate = (id)self; [sheet start]; [windowController() addWindowToWindowList:sheet]; } - (void)welcomeSheet:(TDCWelcomeSheet *)sender onOk:(IRCClientConfig *)config { IRCClient *u = [worldController() createClientWithConfig:config reload:YES]; [mainWindow() expandClient:u]; [worldController() save]; [u connect]; [u selectFirstChannelInChannelList]; } - (void)welcomeSheetWillClose:(TDCWelcomeSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark About Window - (void)showAboutWindow:(id)sender { _popWindowViewIfExists(@"TDCAboutDialog"); TDCAboutDialog *dialog = [TDCAboutDialog new]; dialog.delegate = (id)self; [dialog show]; [windowController() addWindowToWindowList:dialog]; } - (void)aboutDialogWillClose:(TDCAboutDialog *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Server Properties Sheet - (void)showServerPropertiesSheetForClient:(IRCClient *)client withSelection:(TDCServerPropertiesSheetSelection)selection context:(nullable id)context { NSParameterAssert(client != nil); [windowController() popMainWindowSheetIfExists]; TDCServerPropertiesSheet *sheet = [[TDCServerPropertiesSheet alloc] initWithClient:client]; sheet.delegate = self; sheet.window = mainWindow(); [sheet startWithSelection:selection context:context]; [windowController() addWindowToWindowList:sheet]; } - (void)showServerPropertiesSheet:(id)sender { IRCClient *u = self.selectedClient; if (u == nil) { return; } [self showServerPropertiesSheetForClient:u withSelection:TDCServerPropertiesSheetSelectionDefault context:nil]; } - (void)serverPropertiesSheet:(TDCServerPropertiesSheet *)sender onOk:(IRCClientConfig *)config { IRCClient *u = sender.client; if (u == nil) { u = [worldController() createClientWithConfig:config reload:YES]; [mainWindow() expandClient:u]; [worldController() save]; return; } BOOL sameEncoding = (config.primaryEncoding == u.config.primaryEncoding); [u updateConfig:config]; if (sameEncoding == NO) { [mainWindow() reloadTheme]; } [mainWindow() reloadTreeGroup:u]; [worldController() save]; } - (void)serverPropertiesSheetWillClose:(TDCServerPropertiesSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Highlight List Sheet - (void)showServerHighlightList:(id)sender { [windowController() popMainWindowSheetIfExists]; IRCClient *u = self.selectedClient; if (u == nil) { return; } TDCServerHighlightListSheet *sheet = [[TDCServerHighlightListSheet alloc] initWithClient:u]; sheet.delegate = (id)self; sheet.window = mainWindow(); [sheet start]; [windowController() addWindowToWindowList:sheet]; } - (void)serverHighlightListSheetWillClose:(TDCServerHighlightListSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Nickname Color Sheet - (void)memberChangeColor:(NSString *)nickname { NSParameterAssert(nickname != nil); [windowController() popMainWindowSheetIfExists]; IRCClient *u = self.selectedClient; if (u == nil) { return; } TDCNicknameColorSheet *sheet = [[TDCNicknameColorSheet alloc] initWithNickname:nickname]; sheet.delegate = (id)self; sheet.window = mainWindow(); [sheet start]; [windowController() addWindowToWindowList:sheet]; } - (void)nicknameColorSheetOnOk:(TDCNicknameColorSheet *)sender { [mainWindow() reloadTheme]; } - (void)nicknameColorSheetWillClose:(TDCNicknameColorSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Channel Topic Sheet - (void)showChannelModifyTopicSheet:(id)sender { [windowController() popMainWindowSheetIfExists]; IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || c.isChannel == NO) { return; } TDCChannelModifyTopicSheet *sheet = [[TDCChannelModifyTopicSheet alloc] initWithChannel:c]; sheet.delegate = (id)self; sheet.window = mainWindow(); [sheet start]; [windowController() addWindowToWindowList:sheet]; } - (void)channelModifyTopicSheet:(TDCChannelModifyTopicSheet *)sender onOk:(NSString *)topic { IRCClient *u = sender.client; IRCChannel *c = sender.channel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } [u sendTopicTo:topic inChannel:c]; } - (void)channelModifyTopicSheetWillClose:(TDCChannelModifyTopicSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Channel Mode Sheet - (void)showChannelModifyModesSheet:(id)sender { [windowController() popMainWindowSheetIfExists]; IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil || c == nil || c.isChannel == NO) { return; } TDCChannelModifyModesSheet *sheet = [[TDCChannelModifyModesSheet alloc] initWithChannel:c]; sheet.delegate = (id)self; sheet.window = mainWindow(); [sheet start]; [windowController() addWindowToWindowList:sheet]; } - (void)channelModifyModesSheet:(TDCChannelModifyModesSheet *)sender onOk:(IRCChannelModeContainer *)modes { IRCClient *u = sender.client; IRCChannel *c = sender.channel; if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) { return; } NSString *changeString = [c.modeInfo getChangeCommand:modes]; if (changeString.length == 0) { return; } [u sendModes:changeString withParameters:nil inChannel:c]; } - (void)channelModifyModesSheetWillClose:(TDCChannelModifyModesSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Channel Spotlight Window - (void)showChannelSpotlightWindow:(id)sender { _popWindowViewIfExists(@"TDCChannelSpotlightController"); TDCChannelSpotlightController *dialog = [TDCChannelSpotlightController new]; dialog.delegate = (id)self; [dialog show]; [windowController() addWindowToWindowList:dialog]; } - (void)channelSpotlightController:(TDCChannelSpotlightController *)sender selectChannel:(IRCChannel *)channel { [mainWindow() select:channel]; } - (void)channelSpotlightControllerWillClose:(TDCChannelSpotlightController *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Change Nickname Sheet - (void)showServerChangeNicknameSheet:(id)sender { [windowController() popMainWindowSheetIfExists]; IRCClient *u = self.selectedClient; if (u == nil || u.isLoggedIn == NO) { return; } TDCServerChangeNicknameSheet *sheet = [[TDCServerChangeNicknameSheet alloc] initWithClient:u]; sheet.delegate = (id)self; sheet.window = mainWindow(); [sheet start]; [windowController() addWindowToWindowList:sheet]; } - (void)serverChangeNicknameSheet:(TDCServerChangeNicknameSheet *)sender didInputNickname:(NSString *)nickname { IRCClient *u = sender.client; if (u == nil || u.isConnected == NO) { return; } [u changeNickname:nickname]; } - (void)serverChangeNicknameSheetWillClose:(TDCServerChangeNicknameSheet *)sender { [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Preferences Dialog - (void)showPreferencesWindow:(id)sender { [self showPreferencesWindowWithSelection:TDCPreferencesControllerSelectionDefault]; } - (void)showNotificationPreferences:(id)sender { [self showPreferencesWindowWithSelection:TDCPreferencesControllerSelectionNotifications]; } - (void)showStylePreferences:(id)sender { [self showPreferencesWindowWithSelection:TDCPreferencesControllerSelectionStyle]; } - (void)showHiddenPreferences:(id)sender { [self showPreferencesWindowWithSelection:TDCPreferencesControllerSelectionHiddenPreferences]; } - (void)showPreferencesWindowWithSelection:(TDCPreferencesControllerSelection)selection { TDCPreferencesController *openWindow = [windowController() windowFromWindowList:@"TDCPreferencesController"]; if (openWindow) { [openWindow show:selection]; return; } TDCPreferencesController *controller = [TDCPreferencesController new]; controller.delegate = (id)self; [controller show:selection]; [windowController() addWindowToWindowList:controller]; } - (void)preferencesDialogWillClose:(TDCPreferencesController *)sender { [TPCPreferences performReloadAction:(TPCPreferencesReloadActionHighlightKeywords | TPCPreferencesReloadActionPreferencesChanged)]; [windowController() removeWindowFromWindowList:sender]; } @end #pragma mark - #pragma mark Main Window Proxy @implementation TXMenuControllerMainWindowProxy - (void)openStandaloneStoreWebpage:(id)sender { [menuController() openStandaloneStoreWebpage:sender]; } - (void)manageLicense:(id)sender { [menuController() manageLicense:sender]; } - (void)showWelcomeSheet:(id)sender { [menuController() showWelcomeSheet:sender]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Controllers/TXSharedApplication.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "BuildConfig.h" #import "OELReachability.h" #import "TXAppearance.h" #import "TXMasterController.h" #import "TXMenuController.h" #import "TXWindowControllerPrivate.h" #import "TPCThemeController.h" #import "THOPluginManagerPrivate.h" #import "IRCWorld.h" #import "TLOEncryptionManagerPrivate.h" #import "TLONotificationController.h" #import "TLOSpeechSynthesizerPrivate.h" #import "TDCFileTransferDialogPrivate.h" #import "TDCLicenseManagerDialogPrivate.h" #import "TVCLogControllerOperationQueuePrivate.h" #import "TXSharedApplicationPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TXErrorDomain = @"TextualErrorDomain"; #define _defineSharedInstance(si_name, si_class, si_init_method) \ + (si_class *)si_name \ { \ static id sharedSelf = nil; \ \ static dispatch_once_t onceToken; \ \ dispatch_once(&onceToken, ^{ \ sharedSelf = [si_class si_init_method]; \ }); \ \ return sharedSelf; \ } @implementation TXSharedApplication _defineSharedInstance(sharedAppearance, TXAppearance, new) #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 _defineSharedInstance(sharedEncryptionManager, TLOEncryptionManager, new) #endif _defineSharedInstance(sharedNetworkReachabilityNotifier, OELReachability, reachabilityForInternetConnection) _defineSharedInstance(sharedNotificationController, TLONotificationController, new) _defineSharedInstance(sharedPluginManager, THOPluginManager, new) _defineSharedInstance(sharedPrintingQueue, TVCLogControllerPrintingOperationQueue, new) _defineSharedInstance(sharedSpeechSynthesizer, TLOSpeechSynthesizer, new) _defineSharedInstance(sharedThemeController, TPCThemeController, new) _defineSharedInstance(sharedWindowController, TXWindowController, new) #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 _defineSharedInstance(sharedLicenseManagerDialog, TDCLicenseManagerDialog, new) #endif _defineSharedInstance(sharedFileTransferDialog, TDCFileTransferDialog, new) os_log_t ApplicationTerminationLogSubsystem(void) { static os_log_t cachedValue = NULL; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = os_log_create(TXBundleBuildProductIdentifierCString, "Termination"); }); return cachedValue; } @end #pragma mark - @implementation NSObject (TXSharedApplicationObjectExtension) __weak static TXMasterController *TXGlobalMasterControllerClassReference; + (void)setGlobalMasterControllerClassReference:(id)masterController { TXGlobalMasterControllerClassReference = masterController; } - (TXMasterController *)masterController { return TXGlobalMasterControllerClassReference; } + (TXMasterController *)masterController { return TXGlobalMasterControllerClassReference; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Controllers/TXWindowController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" #import "TVCMainWindow.h" #import "TXWindowControllerPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TXWindowController () @property (nonatomic, strong) NSMutableDictionary *windowObjects; @end @implementation TXWindowController - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.windowObjects = [NSMutableDictionary dictionary]; } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Preparing window controller"); @synchronized(self.windowObjects) { [self.windowObjects removeAllObjects]; self.windowObjects = nil; } } + (NSString *)windowDescriptionForWindow:(id)window { return [self windowDescriptionForWindow:window inRelationTo:nil]; } + (NSString *)windowDescriptionForWindow:(id)window inRelationTo:(nullable id)relatedObject { NSParameterAssert(window != nil); NSString *windowClass = NSStringFromClass([window class]); if (relatedObject == nil) { return windowClass; } return [NSString stringWithFormat:@"%@ -> %@", windowClass, [relatedObject description]]; } - (void)addWindowToWindowList:(id)window { [self addWindowToWindowList:window inRelationTo:nil]; } - (void)addWindowToWindowList:(id)window inRelationTo:(nullable id)relatedObject { NSString *windowDescription = [self.class windowDescriptionForWindow:window inRelationTo:relatedObject]; [self addWindowToWindowList:window withDescription:windowDescription]; } - (void)addWindowToWindowList:(id)window withDescription:(NSString *)windowDescription { NSParameterAssert(window != nil); NSAssert([window respondsToSelector:@selector(window)], @"'window' does not respond to -window"); @synchronized(self.windowObjects) { self.windowObjects[windowDescription] = window; } } - (void)removeWindowFromWindowList:(id)window { [self removeWindowFromWindowList:window inRelationTo:nil]; } - (void)removeWindowFromWindowList:(id)window inRelationTo:(nullable id)relatedObject { NSParameterAssert(window != nil); if ([window isKindOfClass:[NSArray class]]) { for (id object in window) { [self removeWindowFromWindowList:object inRelationTo:relatedObject]; } return; // Do not continue... } BOOL windowWasString = NO; NSString *windowDescription = nil; if ([window isKindOfClass:[NSString class]]) { windowWasString = YES; windowDescription = window; } else { windowDescription = [self.class windowDescriptionForWindow:window inRelationTo:relatedObject]; } if (windowDescription == nil) { return; // Cannot continue... } @synchronized(self.windowObjects) { if (self.windowObjects[windowDescription] == nil && windowWasString == NO) { windowDescription = [self.windowObjects firstKeyForObject:window]; } if (windowDescription) { [self.windowObjects removeObjectForKey:windowDescription]; } } } - (nullable id)windowFromWindowList:(NSString *)windowDescription { NSParameterAssert(windowDescription != nil); @synchronized(self.windowObjects) { return self.windowObjects[windowDescription]; } } - (NSArray *)windowsFromWindowList:(NSArray *)windowDescriptions { NSParameterAssert(windowDescriptions != nil); @synchronized(self.windowObjects) { NSMutableArray *returnedValues = [NSMutableArray array]; for (id windowDescription in windowDescriptions) { if ([windowDescription isKindOfClass:[NSString class]] == NO) { continue; } id windowObject = self.windowObjects[windowDescription]; if (windowObject) { [returnedValues addObject:windowObject]; } } return [returnedValues copy]; } } - (BOOL)maybeBringWindowForward:(NSString *)windowDescription { NSParameterAssert(windowDescription != nil); id windowObject = [self windowFromWindowList:windowDescription]; if (windowObject) { NSWindow *window = [windowObject window]; [window makeKeyAndOrderFront:nil]; return YES; } return NO; } - (void)popMainWindowSheetIfExists { NSWindow *attachedSheet = mainWindow().attachedSheet; if (attachedSheet == nil) { return; } [attachedSheet close]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Channel Spotlight/TDCChannelSpotlightAppearance.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TVCAppearancePrivate.h" #import "TDCChannelSpotlightControlsPrivate.h" #import "TDCChannelSpotlightAppearanceInternal.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelSpotlightAppearance () #pragma mark - #pragma mark Search Field @property (nonatomic, copy, nullable, readwrite) NSColor *searchFieldTextColor; @property (nonatomic, copy, nullable, readwrite) NSColor *searchFieldCompletionTextColor; @property (nonatomic, copy, nullable, readwrite) NSColor *searchFieldNoResultsTextColor; #pragma mark - #pragma mark Search Result @property (nonatomic, copy, nullable, readwrite) NSColor *searchResultRowSelectionColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *searchResultRowSelectionColorInactiveWindow; @property (nonatomic, assign, readwrite) BOOL searchResultRowEmphasized; @property (nonatomic, copy, nullable, readwrite) NSColor *searchResultChannelNameTextColor; @property (nonatomic, copy, nullable, readwrite) NSColor *searchResultChannelDescriptionTextColor; @property (nonatomic, copy, nullable, readwrite) NSColor *searchResultKeyboardShortcutTextColor; @property (nonatomic, assign, readwrite) CGFloat searchResultKeyboardShortcutDeselectedOffset; @property (nonatomic, assign, readwrite) CGFloat searchResultKeyboardShortcutSelectedOffset; @property (nonatomic, copy, nullable, readwrite) NSColor *searchResultSelectedTextColor; @end @implementation TDCChannelSpotlightAppearance #pragma mark - #pragma mark Initialization - (nullable instancetype)initWithWindow:(TDCChannelSpotlightPanel *)window { NSParameterAssert(window != nil); NSURL *appearanceLocation = [self.class appearanceLocation]; BOOL forRetinaDisplay = window.runningInHighResolutionMode; if ((self = [super initWithAppearanceAtURL:appearanceLocation forRetinaDisplay:forRetinaDisplay])) { [self prepareInitialState]; return self; } return nil; } + (NSURL *)appearanceLocation { return [RZMainBundle() URLForResource:@"TDCChannelSpotlightAppearance" withExtension:@"plist"]; } - (void)prepareInitialState { NSDictionary *properties = self.appearanceProperties; NSDictionary *searchField = properties[@"Search Field"]; self.searchFieldTextColor = [self colorInGroup:searchField withKey:@"controlTextColor"]; self.searchFieldCompletionTextColor = [self colorInGroup:searchField withKey:@"completionTextColor"]; self.searchFieldNoResultsTextColor = [self colorInGroup:searchField withKey:@"noResultsTextColor"]; NSDictionary *searchResult = properties[@"Search Result"]; self.searchResultRowSelectionColorActiveWindow = [self colorInGroup:searchResult withKey:@"selectionColor" forActiveWindow:YES]; self.searchResultRowSelectionColorInactiveWindow = [self colorInGroup:searchResult withKey:@"selectionColor" forActiveWindow:NO]; self.searchResultRowEmphasized = [searchResult boolForKey:@"rowEmphasized"]; self.searchResultChannelNameTextColor = [self colorInGroup:searchResult withKey:@"channelNameTextColor"]; self.searchResultChannelDescriptionTextColor = [self colorInGroup:searchResult withKey:@"channelDescriptionTextColor"]; self.searchResultKeyboardShortcutTextColor = [self colorInGroup:searchResult withKey:@"keyboardShortcutTextColor"]; self.searchResultKeyboardShortcutDeselectedOffset = [self measurementInGroup:searchResult withKey:@"keyboardShortcutDeselectedOffset"]; self.searchResultKeyboardShortcutSelectedOffset = [self measurementInGroup:searchResult withKey:@"keyboardShortcutSelectedOffset"]; self.searchResultSelectedTextColor = [self colorInGroup:searchResult withKey:@"selectedTextColor"]; [self flushAppearanceProperties]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Channel Spotlight/TDCChannelSpotlightController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXMasterController.h" #import "IRCAddressBookUserTracking.h" #import "IRCClient.h" #import "IRCWorld.h" #import "TPCPreferencesLocal.h" #import "TPCPreferencesUserDefaults.h" #import "TLOLicenseManagerPrivate.h" #import "TLOLocalization.h" #import "TVCMainWindowPrivate.h" #import "TDCLicenseManagerDialogPrivate.h" #import "TDCChannelSpotlightAppearanceInternal.h" #import "TDCChannelSpotlightSearchResultPrivate.h" #import "TDCChannelSpotlightSearchResultsTablePrivate.h" #import "TDCChannelSpotlightControlsPrivate.h" #import "TDCChannelSpotlightControllerInternal.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelSpotlightController () @property (nonatomic, strong, readwrite) TDCChannelSpotlightAppearance *userInterfaceObjects; @property (nonatomic, weak) IBOutlet NSVisualEffectView *visualEffectView; @property (nonatomic, weak) IBOutlet NSTextField *noResultsLabel; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *noResultsLabelLeadingConstraint; @property (nonatomic, weak) IBOutlet NSView *searchResultsView; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *searchResultsViewHeightConstraint; @property (nonatomic, weak) IBOutlet NSTextField *searchField; @property (nonatomic, weak) IBOutlet NSTableView *searchResultsTable; @property (nonatomic, strong) IBOutlet NSArrayController *searchResultsController; @property (nonatomic, strong) id mouseEventMonitor; @end @implementation TDCChannelSpotlightController #pragma mark - #pragma mark Initialization - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCChannelSpotlightController" owner:self topLevelObjects:nil]; self.searchResultsTable.doubleAction = @selector(delegatePostSelectChannelForDoubleClickedRow:); self.mouseEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^NSEvent *(NSEvent *event) { return [self respondToKeyDownEvent:event]; }]; self.searchResultsController.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"distance" ascending:NO selector:@selector(compare:)] ]; [RZNotificationCenter() addObserver:self selector:@selector(applicationAppearanceChanged:) name:TXApplicationAppearanceChangedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(channelListChanged:) name:IRCClientChannelListWasModifiedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(clientListChanged:) name:IRCWorldClientListWasModifiedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(mainWindowSelectionChanged:) name:TVCMainWindowSelectionChangedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(preferencesChanged:) name:TPCPreferencesUserDefaultsDidChangeNotification object:nil]; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 [RZNotificationCenter() addObserver:self selector:@selector(licenseManagerDeactivatedLicense:) name:TDCLicenseManagerDeactivatedLicenseNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(licenseManagerTrialExpired:) name:TDCLicenseManagerTrialExpiredNotification object:nil]; #endif [self populateArrayController]; [self applicationAppearanceChanged]; [self.noResultsLabelLeadingConstraint archiveConstant]; [self.searchResultsViewHeightConstraint archiveConstant]; [self updatePredicate]; } #pragma mark - #pragma mark Appearance - (void)applicationAppearanceChanged:(NSNotification *)notification { [self applicationAppearanceChanged]; } - (void)applicationAppearanceChanged { TDCChannelSpotlightAppearance *appearance = [[TDCChannelSpotlightAppearance alloc] initWithWindow:(id)self.window]; self.userInterfaceObjects = appearance; [self updateVibrancyWithAppearance:appearance]; [self updateControlsWithAppearance:appearance]; [self updateSearchResultsSelection]; } - (void)updateVibrancyWithAppearance:(TDCChannelSpotlightAppearance *)appearance { NSParameterAssert(appearance != nil); NSAppearance *appKitAppearance = appearance.appKitAppearance; switch (appearance.appKitAppearanceTarget) { case TXAppKitAppearanceTargetWindow: { self.window.appearance = appKitAppearance; break; } default: { break; } } // switch() } - (void)updateControlsWithAppearance:(TDCChannelSpotlightAppearance *)appearance { NSParameterAssert(appearance != nil); self.searchField.textColor = appearance.searchFieldTextColor; self.noResultsLabel.textColor = appearance.searchFieldNoResultsTextColor; } - (void)updateControlsState { NSString *searchString = self.searchField.stringValue; if (searchString.length == 0) { self.noResultsLabel.stringValue = @""; [self.noResultsLabelLeadingConstraint zeroOutConstant]; [self.searchResultsViewHeightConstraint zeroOutConstant]; return; } if (self.searchResultsCount == 0) { self.noResultsLabel.stringValue = TXTLS(@"TDCChannelSpotlightController[tyv-p6]"); [self.noResultsLabelLeadingConstraint restoreArchivedConstant]; [self.searchResultsViewHeightConstraint zeroOutConstant]; return; } self.noResultsLabel.stringValue = @""; [self.noResultsLabelLeadingConstraint zeroOutConstant]; [self.searchResultsViewHeightConstraint restoreArchivedConstant]; [self selectFirstSearchResultIfNecessary]; } - (void)updateSearchResultsSelection { NSTableView *table = self.searchResultsTable; [table invalidateBackgroundForSelection]; } #pragma mark - #pragma mark Events - (nullable NSEvent *)respondToKeyDownEvent:(NSEvent *)event { if (self.window.isKeyWindow == NO) { return event; } switch (event.keyCode) { case 18 ... 23: // 0-9 (top row) case 25 ... 26: case 28 ... 29: case 82 ... 92: // 0-9 (number pad) { NSUInteger keyboardKeys = (event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask); keyboardKeys &= ~NSEventModifierFlagNumericPad; if (keyboardKeys == NSEventModifierFlagCommand) { return [self handleCommandNumberEvent:event]; } return event; } case 36: // return case 76: // enter { [self delegatePostSelectChannelForSelectedRow]; return nil; } case 53: // escape { [self clearSearchStringOrClose]; return nil; } case 126: // arrow up case 125: // arrow down case 116: // page up case 121: // page down { return [self handlePageUpDownEvent:event]; } } return event; } - (nullable NSEvent *)handlePageUpDownEvent:(NSEvent *)event { NSUInteger searchResultsCount = self.searchResultsCount; if (searchResultsCount == 0) { return nil; } NSInteger selectedRow = self.searchResultsTable.selectedRow; // Wrap around table when we reach the top or bottom if (event.keyCode == 126 || event.keyCode == 116) { // up if (selectedRow == 0) { [self.searchResultsTable selectItemAtIndex:(searchResultsCount - 1)]; return nil; } } else if (event.keyCode == 125 || event.keyCode == 121) { // down if (selectedRow == (searchResultsCount - 1)) { [self.searchResultsTable selectItemAtIndex:0]; return nil; } } [self.searchResultsTable keyDown:event]; return nil; } - (nullable NSEvent *)handleCommandNumberEvent:(NSEvent *)event { NSInteger numberPressed = event.characters.integerValue; if (numberPressed < 0 || numberPressed > 9) { return event; } NSInteger arrayIndex = (numberPressed - 1); if (arrayIndex < 0) { arrayIndex = 9; } [self delegatePostSelectChannelForSearchResultAtIndex:arrayIndex]; return nil; } - (void)delegatePostSelectChannelForDoubleClickedRow:(id)sender { NSInteger clickedRow = self.searchResultsTable.clickedRow; [self delegatePostSelectChannelForSearchResultAtIndex:clickedRow]; } - (void)delegatePostSelectChannelForSelectedRow { NSInteger selectedRow = self.searchResultsTable.selectedRow; [self delegatePostSelectChannelForSearchResultAtIndex:selectedRow]; } - (void)delegatePostSelectChannelForSearchResultAtIndex:(NSInteger)searchResultIndex { NSArray *searchResults = self.searchResultsFiltered; if (searchResultIndex < 0 || searchResultIndex >= searchResults.count) { return; } TDCChannelSpotlightSearchResult *searchResult = searchResults[searchResultIndex]; IRCChannel *channel = searchResult.channel; [self delegatePostSelectChannel:channel]; } - (void)delegatePostSelectChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ([self.delegate respondsToSelector:@selector(channelSpotlightController:selectChannel:)]) { [self.delegate channelSpotlightController:self selectChannel:channel]; } [self close]; } #pragma mark - #pragma mark Window Management - (void)clearSearchStringOrClose { /* Mimic Spotlight behavior by clearing search string on first escape and closing on second escape. */ if (self.searchField.stringValue.length > 0) { self.searchField.stringValue = @""; [self searchStringChanged]; return; } [self close]; } - (void)close { [self saveWindowFrame]; [self.window close]; } - (void)show { [self restoreWindowFrame]; [self.window makeKeyAndOrderFront:nil]; } - (void)restoreWindowFrame { NSWindow *window = self.window; [window saveSizeAsDefault]; [window restoreWindowStateForClass:self.class]; } - (void)saveWindowFrame { /* Reset search back to none before closing so that the frame we save is same we open. */ [self resetSearch]; NSWindow *window = self.window; /* We call -restoreDefaultSizeAndDisplay: before saving the frame because the window wont register the changes to the constants in -resetSearch until next layout pass. */ [window restoreDefaultSizeAndDisplay:NO]; [window saveWindowStateForClass:self.class]; } #pragma mark - #pragma mark Search Field - (NSString *)searchString { return self.searchField.stringValue; } #pragma mark - #pragma mark Search Results - (void)updatePredicate { if ([TPCPreferences channelNavigationIsServerSpecific]) { NSString *clientId = mainWindow().selectedClient.uniqueIdentifier; self.searchResultsController.filterPredicate = [NSPredicate predicateWithFormat:@"distance >= 0.5 && clientId LIKE[c] %@", clientId]; } else { self.searchResultsController.filterPredicate = [NSPredicate predicateWithFormat:@"distance >= 0.5"]; } [self updateControlsState]; } - (void)resetSearch { self.searchField.stringValue = @""; [self searchStringChanged]; } - (void)selectFirstSearchResultIfNecessary { NSInteger selectedRow = self.searchResultsTable.selectedRow; if (selectedRow < 0) { [self.searchResultsTable selectItemAtIndex:0]; } } - (NSArray *)searchResults { return self.searchResultsController.content; } - (NSArray *)searchResultsFiltered { return self.searchResultsController.arrangedObjects; } - (NSUInteger)searchResultsCount { return ((NSArray *)self.searchResultsController.arrangedObjects).count; } - (NSInteger)selectedSearchResult { return self.searchResultsTable.selectedRow; } - (void)recalculateDistanceForSearchResults { NSString *searchString = self.searchField.stringValue; NSArray *searchResults = self.searchResults; for (TDCChannelSpotlightSearchResult *searchResult in searchResults) { [searchResult recalculateDistanceWith:searchString]; } [self updateControlsState]; } - (void)populateArrayController { NSPredicate *filterPredicate = self.searchResultsController.filterPredicate; self.searchResultsController.filterPredicate = nil; NSString *searchString = self.searchField.stringValue; NSMutableArray *searchResults = [NSMutableArray array]; for (IRCClient *client in worldController().clientList) { for (IRCChannel *channel in client.channelList) { TDCChannelSpotlightSearchResult *searchResult = [self searchResultForChannel:channel]; [searchResult recalculateDistanceWith:searchString]; [searchResults addObject:searchResult]; } } [self.searchResultsController removeAllArrangedObjects]; [self.searchResultsController addObjects:searchResults]; self.searchResultsController.filterPredicate = filterPredicate; } - (TDCChannelSpotlightSearchResult *)searchResultForChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); TDCChannelSpotlightSearchResult *searchResult = [[TDCChannelSpotlightSearchResult alloc] initWithChannel:channel]; return searchResult; } - (void)searchStringChanged { [self recalculateDistanceForSearchResults]; } #pragma mark - #pragma mark Table View Delegate - (nullable NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row { return [[TDCChannelSpotlightSearchResultRowView alloc] initWithController:self]; } #pragma mark - #pragma mark Notifications - (void)mainWindowSelectionChanged:(NSNotification *)notification { /* Predicate is updated when selection changes because predicate may be configured to be per-server. This could be made more efficient by checking if it is server specific and comparing the selected client identifier to what is in predicate. That involves a lot more work for something that shouldn't be fired a lot. */ [self updatePredicate]; } - (void)preferencesChanged:(NSNotification *)notification { NSString *changedKey = notification.userInfo[@"changedKey"]; if ([changedKey isEqualToString:@"ChannelNavigationIsServerSpecific"]) { [self updatePredicate]; } } - (void)clientListChanged:(id)sender { [self populateArrayController]; [self updateControlsState]; } - (void)channelListChanged:(id)sender { [self populateArrayController]; [self updateControlsState]; } - (void)controlTextDidChange:(NSNotification *)notification { if (notification.object == self.searchField) { [self searchStringChanged]; } } #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 - (void)licenseManagerDeactivatedLicense:(NSNotification *)notification { if (TLOLicenseManagerIsTrialExpired() == NO) { return; } [self close]; } - (void)licenseManagerTrialExpired:(NSNotification *)notification { [self close]; } #endif - (void)windowWillClose:(NSNotification *)notification { [NSEvent removeMonitor:self.mouseEventMonitor]; self.mouseEventMonitor = nil; [RZNotificationCenter() removeObserver:self]; if ([self.delegate respondsToSelector:@selector(channelSpotlightControllerWillClose:)]) { [self.delegate channelSpotlightControllerWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Channel Spotlight/TDCChannelSpotlightControls.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCChannelSpotlightControlsPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TDCChannelSpotlightPanel - (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag { if ((self = [super initWithContentRect:contentRect styleMask:style backing:bufferingType defer:flag])) { [self prepareInitialState]; } return self; } - (void)prepareInitialState { self.styleMask = (self.styleMask | NSWindowStyleMaskFullSizeContentView); self.titlebarAppearsTransparent = YES; self.titleVisibility = NSWindowTitleHidden; [self standardWindowButton:NSWindowCloseButton].hidden = YES; [self standardWindowButton:NSWindowMiniaturizeButton].hidden = YES; [self standardWindowButton:NSWindowZoomButton].hidden = YES; } - (BOOL)isMovable { return YES; } - (BOOL)isMovableByWindowBackground { return YES; } - (BOOL)canBecomeKeyWindow { return YES; } - (BOOL)canBecomeMainWindow { return YES; } @end #pragma mark - @implementation TDCChannelSpotlightTextField - (BOOL)mouseDownCanMoveWindow { return YES; } @end #pragma mark - @implementation TDCChannelSpotlightImageView - (BOOL)mouseDownCanMoveWindow { return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Channel Spotlight/TDCChannelSpotlightSearchResult.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCClient.h" #import "IRCChannel.h" #import "TDCChannelSpotlightSearchResultPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelSpotlightSearchResult () @property (nonatomic, weak, readwrite) IRCChannel *channel; @property (nonatomic, copy, readwrite) NSNumber *distance; @end @implementation TDCChannelSpotlightSearchResult - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ((self = [super init])) { self.channel = channel; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.distance = @(0.0); } - (NSComparisonResult)compare:(TDCChannelSpotlightSearchResult *)other { double localDistance = self.distance.doubleValue; double remoteDistance = other.distance.doubleValue; if (localDistance > remoteDistance) { return NSOrderedAscending; } else if (localDistance < remoteDistance) { return NSOrderedDescending; } return NSOrderedSame; } - (void)recalculateDistanceWith:(NSString *)searchString { NSParameterAssert(searchString != nil); if (searchString.length == 0) { self.distance = @(0.0); return; } NSString *searchableString = self.channel.name; double distance = [searchableString compareWithWord:searchString lengthPenaltyWeight:1.0]; self.distance = @(distance); } - (NSString *)clientId { return self.channel.associatedClient.uniqueIdentifier; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Channel Spotlight/TDCChannelSpotlightSearchResultsTable.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXGlobalModels.h" #import "NSViewHelper.h" #import "TLOLocalization.h" #import "IRCClient.h" #import "IRCChannel.h" #import "TDCChannelSpotlightAppearancePrivate.h" #import "TDCChannelSpotlightControllerInternal.h" #import "TDCChannelSpotlightSearchResultPrivate.h" #import "TDCChannelSpotlightSearchResultsTablePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelSpotlightSearchResultRowView () @property (nonatomic, weak) TDCChannelSpotlightController *controller; @property (nonatomic, weak) TDCChannelSpotlightSearchResultCellView *childCell; @property (readonly) TDCChannelSpotlightAppearance *userInterfaceObjects; @end @interface TDCChannelSpotlightSearchResultCellView () @property (readonly, copy) NSAttributedString *channelName; @property (readonly, copy) NSString *keyboardShortcut; @property (readonly, copy) NSString *unreadCountDescription; @property (nonatomic, weak) IBOutlet NSTextField *channelNameField; @property (nonatomic, weak) IBOutlet NSTextField *keyboardShortcutField; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *keyboardShortcutFieldOffsetConstraint; @property (nonatomic, weak) IBOutlet NSTextField *unreadCountDescriptionField; @property (nonatomic, assign) BOOL staticLabelsPopulated; @property (readonly) TDCChannelSpotlightAppearance *userInterfaceObjects; @property (readonly) TDCChannelSpotlightController *controller; @property (readonly) TDCChannelSpotlightSearchResultRowView *rowCell; @end @implementation TDCChannelSpotlightSearchResultCellView - (BOOL)wantsLayer { return YES; } - (NSViewLayerContentsRedrawPolicy)layerContentsRedrawPolicy { return NSViewLayerContentsRedrawOnSetNeedsDisplay; } - (void)setInitialValues { [self channelNameChanged]; } - (void)updateLayer { [self updateAppearance]; [self keyboardShortcutChanged]; } - (void)updateAppearance { TDCChannelSpotlightSearchResultRowView *rowCell = self.rowCell; [self updateTextFieldTextColorIsSelected:rowCell.isSelected]; } - (void)updateTextFieldTextColorIsSelected:(BOOL)isSelected { TDCChannelSpotlightAppearance *appearance = self.userInterfaceObjects; if (isSelected == NO) { self.channelNameField.textColor = appearance.searchResultChannelNameTextColor; self.unreadCountDescriptionField.textColor = appearance.searchResultChannelDescriptionTextColor; self.keyboardShortcutField.textColor = appearance.searchResultKeyboardShortcutTextColor; self.keyboardShortcutFieldOffsetConstraint.constant = appearance.searchResultKeyboardShortcutDeselectedOffset; } else { NSColor *selectedTextColor = appearance.searchResultSelectedTextColor; self.channelNameField.textColor = selectedTextColor; self.unreadCountDescriptionField.textColor = selectedTextColor; self.keyboardShortcutField.textColor = selectedTextColor; self.keyboardShortcutFieldOffsetConstraint.constant = appearance.searchResultKeyboardShortcutSelectedOffset; } } - (NSAttributedString *)channelName { TDCChannelSpotlightSearchResult *searchResult = self.objectValue; if (searchResult == nil) { return [NSAttributedString attributedString]; } static NSMutableParagraphStyle *paragraphStyle = nil; if (paragraphStyle == nil) { paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail; } NSString *channelName = searchResult.channel.name; NSFont *channelNameFieldFont = self.channelNameField.font; NSMutableAttributedString *resultString = [NSMutableAttributedString mutableAttributedStringWithString:TXTLS(@"TDCChannelSpotlightController[jpw-cj]", channelName) attributes:@{ NSFontAttributeName : channelNameFieldFont, NSParagraphStyleAttributeName : paragraphStyle }]; TDCChannelSpotlightController *controller = self.controller; NSString *searchString = controller.searchString; [resultString.string enumerateFirstOccurrenceOfCharactersInString:searchString withBlock:^(NSRange range, BOOL *stop) { NSFont *boldFont = [RZFontManager() convertFont:channelNameFieldFont toHaveTrait:NSBoldFontMask]; [resultString addAttribute:NSFontAttributeName value:boldFont range:range]; } options:NSCaseInsensitiveSearch]; NSString *networkName = searchResult.channel.associatedClient.networkNameAlt; [resultString appendString:TXTLS(@"TDCChannelSpotlightController[z68-5q]", networkName)]; return resultString; } - (void)channelNameChanged { [self willChangeValueForKey:@"channelName"]; [self didChangeValueForKey:@"channelName"]; } - (NSString *)keyboardShortcut { TDCChannelSpotlightSearchResult *searchResult = self.objectValue; if (searchResult == nil) { return @""; } TDCChannelSpotlightController *controller = self.controller; NSArray *searchResults = controller.searchResultsFiltered; NSUInteger searchResultIndex = [searchResults indexOfObjectIdenticalTo:searchResult]; if (searchResultIndex == controller.selectedSearchResult) { return @"↩︎"; } if (searchResultIndex > 9) { return @""; } NSUInteger keyboardShortcutIndex = (searchResultIndex + 1); if (keyboardShortcutIndex == 10) { keyboardShortcutIndex = 0; } return [NSString stringWithFormat:@"⌘%lu", keyboardShortcutIndex]; } - (void)keyboardShortcutChanged { [self willChangeValueForKey:@"keyboardShortcut"]; [self didChangeValueForKey:@"keyboardShortcut"]; } - (NSString *)unreadCountDescription { TDCChannelSpotlightSearchResult *searchResult = self.objectValue; if (searchResult == nil) { return @""; } NSUInteger nicknameHighlightCount = searchResult.channel.nicknameHighlightCount; NSString *nicknameHighlightCountDescription = nil; if (nicknameHighlightCount == 1) { nicknameHighlightCountDescription = TXTLS(@"TDCChannelSpotlightController[0lz-oh]",TXFormattedNumber(nicknameHighlightCount)); } else { nicknameHighlightCountDescription = TXTLS(@"TDCChannelSpotlightController[c4u-21]", TXFormattedNumber(nicknameHighlightCount)); } NSUInteger unreadCount = searchResult.channel.treeUnreadCount; NSString *unreadCountDescription = nil; if (unreadCount == 1) { unreadCountDescription = TXTLS(@"TDCChannelSpotlightController[43s-x4]", TXFormattedNumber(unreadCount)); } else { unreadCountDescription = TXTLS(@"TDCChannelSpotlightController[vzj-30]", TXFormattedNumber(unreadCount)); } return TXTLS(@"TDCChannelSpotlightController[et7-c5]", nicknameHighlightCountDescription, unreadCountDescription); } - (void)unreadCountDescriptionChanged { [self willChangeValueForKey:@"unreadCountDescription"]; [self didChangeValueForKey:@"unreadCountDescription"]; } - (TDCChannelSpotlightSearchResultRowView *)rowCell { return (id)self.superview; } - (TDCChannelSpotlightAppearance *)userInterfaceObjects { return self.rowCell.userInterfaceObjects; } - (TDCChannelSpotlightController *)controller { return self.rowCell.controller; } - (void)viewDidMoveToWindow { [super viewDidMoveToWindow]; TDCChannelSpotlightSearchResult *searchResult = self.objectValue; IRCChannel *channel = searchResult.channel; if (self.window == nil) { [channel removeObserver:self forKeyPath:@"nicknameHighlightCount"]; [channel removeObserver:self forKeyPath:@"treeUnreadCount"]; } else { [channel addObserver:self forKeyPath:@"nicknameHighlightCount" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:nil]; [channel addObserver:self forKeyPath:@"treeUnreadCount" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:nil]; [self setInitialValues]; } } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:@"nicknameHighlightCount"] || [keyPath isEqualToString:@"treeUnreadCount"]) { [self unreadCountDescriptionChanged]; } } @end #pragma mark - @implementation TDCChannelSpotlightSearchResultRowView - (instancetype)initWithController:(TDCChannelSpotlightController *)controller { NSParameterAssert(controller != nil); if ((self = [super initWithFrame:NSZeroRect])) { self.controller = controller; return self; } return nil; } - (void)setSelected:(BOOL)selected { super.selected = selected; if (selected == NO && self.invalidatingBackgroundForSelection) { return; } [self setNeedsDisplayOnChild]; } - (void)setNeedsDisplayOnChild { self.childCell.needsDisplay = YES; } - (void)drawSelectionInRect:(NSRect)dirtyRect { if ([self needsToDrawRect:dirtyRect] == NO) { return; } BOOL isWindowActive = self.window.isActiveForDrawing; TDCChannelSpotlightAppearance *appearance = self.userInterfaceObjects; NSColor *selectionColor = nil; if (isWindowActive) { selectionColor = appearance.searchResultRowSelectionColorActiveWindow; } else { selectionColor = appearance.searchResultRowSelectionColorInactiveWindow; } // isWindowActive if (selectionColor) { [selectionColor set]; NSRect selectionRect = self.bounds; NSRectFill(selectionRect); } else { [super drawSelectionInRect:dirtyRect]; } // selectionColor } - (BOOL)isEmphasized { TDCChannelSpotlightAppearance *appearance = self.userInterfaceObjects; NSWindow *window = self.window; return (appearance.searchResultRowEmphasized && (window == nil || window.isKeyWindow)); } - (nullable TDCChannelSpotlightSearchResultCellView *)childCell { if (self->_childCell == nil) { if (self.numberOfColumns == 0) { return nil; } self->_childCell = [self viewAtColumn:0]; } return self->_childCell; } - (TDCChannelSpotlightAppearance *)userInterfaceObjects { return self.controller.userInterfaceObjects; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/File Transfers/TDCFileTransferDialog.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TPCPathInfo.h" #import "TPCPreferencesLocal.h" #import "TPCPreferencesUserDefaults.h" #import "TLOInternetAddressLookup.h" #import "TLOLocalization.h" #import "TLOTimer.h" #import "IRCWorld.h" #import "TVCBasicTableView.h" #import "TDCFileTransferDialogTableCellPrivate.h" #import "TDCFileTransferDialogTransferControllerPrivate.h" #import "TDCFileTransferDialogInternal.h" NS_ASSUME_NONNULL_BEGIN /* Refuse to have more than X number of items incoming at any given time. */ #define _addReceiverHardLimit 120 @interface TDCFileTransferDialog () @property (nonatomic, weak) IBOutlet NSButton *clearButton; @property (nonatomic, weak) IBOutlet NSSegmentedCell *navigationControllerCell; @property (nonatomic, weak, readwrite) IBOutlet TVCBasicTableView *fileTransferTable; @property (nonatomic, strong) IBOutlet NSArrayController *fileTransfersController; @property (nonatomic, strong, nullable) TLOInternetAddressLookup *IPAddressRequest; @property (readonly) TDCFileTransferDialogSelection navigationSelection; @property (nonatomic, strong) TLOTimer *maintenanceTimer; @property (nonatomic, copy, nullable) NSURL *downloadDestinationURLPrivate; - (IBAction)hideWindow:(id)sender; - (IBAction)clear:(id)sender; - (IBAction)startTransferOfFile:(id)sender; - (IBAction)stopTransferOfFile:(id)sender; - (IBAction)removeTransferFromList:(id)sender; - (IBAction)openReceivedFile:(id)sender; - (IBAction)revealReceivedFileInFinder:(id)sender; - (IBAction)navigationSelectionDidChange:(id)sender; @end @implementation TDCFileTransferDialog - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; } return self; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCFileTransferDialog" owner:self topLevelObjects:nil]; self.maintenanceTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onMaintenanceTimer]; } onQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; [RZNotificationCenter() addObserver:self selector:@selector(clientWillBeDestroyed:) name:IRCWorldWillDestroyClientNotification object:nil]; } - (void)dealloc { [RZNotificationCenter() removeObserver:self]; [self.maintenanceTimer stop]; self.maintenanceTimer = nil; } - (void)show { [self show:YES restorePosition:YES]; } - (void)show:(BOOL)makeKeyWindow { [self show:makeKeyWindow restorePosition:YES]; } - (void)show:(BOOL)makeKeyWindow restorePosition:(BOOL)restorePosition { if (makeKeyWindow) { [self.window makeKeyAndOrderFront:nil]; } else { [self.window orderFront:nil]; } if (restorePosition) { [self.window restoreWindowStateForClass:self.class]; } } - (nullable TDCFileTransferDialogTransferController *)fileTransferMatchingPort:(uint16_t)port { TDCFileTransferDialogTransferController *fileTransfer = [self fileTransferMatchingCondition:^BOOL(TDCFileTransferDialogTransferController *controller) { return (controller.hostPort == port); }]; return fileTransfer; } - (nullable TDCFileTransferDialogTransferController *)fileTransferWithUniqueIdentifier:(NSString *)identifier { NSParameterAssert(identifier != nil); TDCFileTransferDialogTransferController *fileTransfer = [self fileTransferMatchingCondition:^BOOL(TDCFileTransferDialogTransferController *controller) { return [identifier isEqualToString:controller.uniqueIdentifier]; }]; return fileTransfer; } - (BOOL)fileTransferExistsWithToken:(NSString *)transferToken { NSParameterAssert(transferToken != nil); TDCFileTransferDialogTransferController *fileTransfer = [self fileTransferMatchingCondition:^BOOL(TDCFileTransferDialogTransferController *controller) { return [transferToken isEqualToString:controller.transferToken]; }]; return (fileTransfer != nil); } - (nullable TDCFileTransferDialogTransferController *)fileTransferSenderMatchingToken:(NSString *)transferToken { NSParameterAssert(transferToken != nil); TDCFileTransferDialogTransferController *fileTransfer = [self fileTransferMatchingCondition:^BOOL(TDCFileTransferDialogTransferController *controller) { return ([transferToken isEqualToString:controller.transferToken] && controller.isSender); }]; return fileTransfer; } - (nullable TDCFileTransferDialogTransferController *)fileTransferReceiverMatchingToken:(NSString *)transferToken { NSParameterAssert(transferToken != nil); TDCFileTransferDialogTransferController *fileTransfer = [self fileTransferMatchingCondition:^BOOL(TDCFileTransferDialogTransferController *controller) { return ([transferToken isEqualToString:controller.transferToken] && controller.isSender == NO); }]; return fileTransfer; } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Stopping access to download destination bookmark"); if (self.downloadDestinationURLPrivate) { [self.downloadDestinationURLPrivate stopAccessingSecurityScopedResource]; } LogToConsoleTerminationProgress("Closing file transfer window"); [self close]; LogToConsoleTerminationProgress("Preparing all file transfers for destruction"); [self enumerateFileTransfers:^(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop) { [fileTransfer prepareForPermanentDestruction]; }]; } - (nullable NSString *)addReceiverForClient:(IRCClient *)client nickname:(NSString *)nickname address:(NSString *)hostAddress port:(uint16_t)hostPort filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken { NSParameterAssert(client != nil); NSParameterAssert(nickname != nil); NSParameterAssert(hostAddress != nil); NSParameterAssert(filename != nil); /* A hard limit exists to prevent a bad person continuously sending file transfers which appear in the file transfer, exhausting resources. */ if ([self receiverCount] > _addReceiverHardLimit) { LogToConsoleError("Max receiver count of %{public}i exceeded", _addReceiverHardLimit); return nil; } TDCFileTransferDialogTransferController *controller = [TDCFileTransferDialogTransferController receiverForClient:client nickname:nickname address:hostAddress port:hostPort filename:filename filesize:totalFilesize token:transferToken]; if (controller == nil) { return nil; } [self show:NO restorePosition:NO]; [self addFileTransfer:controller]; NSString *savePath = self.downloadDestinationURLPrivate.path; if ([TPCPreferences fileTransferRequestReplyAction] == TXFileTransferRequestReplyAutomaticallyDownload) { if (savePath == nil) { savePath = [TPCPathInfo userDownloads]; } [controller openWithPath:savePath]; } return controller.uniqueIdentifier; } - (nullable NSString *)addSenderForClient:(IRCClient *)client nickname:(NSString *)nickname path:(NSString *)path autoOpen:(BOOL)autoOpen { NSParameterAssert(client != nil); NSParameterAssert(nickname != nil); NSParameterAssert(path != nil); TDCFileTransferDialogTransferController *controller = [TDCFileTransferDialogTransferController senderForClient:client nickname:nickname path:path]; if (controller == nil) { return nil; } [self show:YES restorePosition:NO]; [self addFileTransfer:controller]; if (autoOpen) { [controller open]; } return controller.uniqueIdentifier; } - (void)updateClearButton { NSArray *stoppedFileTransfers = [self stoppedFileTransfers]; self.clearButton.enabled = (stoppedFileTransfers.count > 0); } - (void)addFileTransfer:(TDCFileTransferDialogTransferController *)controller { /* Resetting the predicate the each time a controller is added is stupid, but this is a low frequency task that we can forgive. */ NSPredicate *filterPredicate = self.fileTransfersController.filterPredicate; self.fileTransfersController.filterPredicate = nil; [self.fileTransfersController insertObject:controller atArrangedObjectIndex:0]; self.fileTransfersController.filterPredicate = filterPredicate; } - (void)removeFileTransfersMatchingClient:(IRCClient *)client { NSParameterAssert(client != nil); NSArray *fileTransfers = [self fileTransfersMatchingClient:client]; if (fileTransfers.count == 0) { return; } [fileTransfers makeObjectsPerformSelector:@selector(prepareForPermanentDestruction)]; [self.fileTransfersController removeObjects:fileTransfers]; } #pragma mark - #pragma mark Notifications - (void)clientWillBeDestroyed:(NSNotification *)notification { [self removeFileTransfersMatchingClient:notification.object]; } #pragma mark - #pragma mark Actions - (BOOL)validateMenuItem:(NSMenuItem *)item { NSInteger tag = item.tag; NSArray *selectedFileTransfers = [self selectedFileTransfers]; if (selectedFileTransfers.count == 0) { return NO; } /* Begin actual validation. */ switch (tag) { case 3001: // Start Download { for (TDCFileTransferDialogTransferController *fileTransfer in selectedFileTransfers) { TDCFileTransferDialogTransferStatus transferStatus = fileTransfer.transferStatus; if (transferStatus == TDCFileTransferDialogTransferStatusStopped || transferStatus == TDCFileTransferDialogTransferStatusRecoverableError) { return YES; } } return NO; } case 3003: // Stop Download { for (TDCFileTransferDialogTransferController *fileTransfer in selectedFileTransfers) { TDCFileTransferDialogTransferStatus transferStatus = fileTransfer.transferStatus; if (transferStatus == TDCFileTransferDialogTransferStatusConnecting || transferStatus == TDCFileTransferDialogTransferStatusReceiving || transferStatus == TDCFileTransferDialogTransferStatusIsListeningAsSender || transferStatus == TDCFileTransferDialogTransferStatusIsListeningAsReceiver || transferStatus == TDCFileTransferDialogTransferStatusSending || transferStatus == TDCFileTransferDialogTransferStatusMappingListeningPort || transferStatus == TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress || transferStatus == TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept || transferStatus == TDCFileTransferDialogTransferStatusWaitingForResumeAccept) { return YES; } } return NO; } case 3004: // Remove Item { return YES; } case 3005: // Open File { for (TDCFileTransferDialogTransferController *fileTransfer in selectedFileTransfers) { TDCFileTransferDialogTransferStatus transferStatus = fileTransfer.transferStatus; if (fileTransfer.isSender != NO) { continue; } if (transferStatus == TDCFileTransferDialogTransferStatusComplete) { return YES; } } return NO; } case 3006: // Reveal In Finder { for (TDCFileTransferDialogTransferController *fileTransfer in selectedFileTransfers) { TDCFileTransferDialogTransferStatus transferStatus = fileTransfer.transferStatus; if (fileTransfer.isSender != NO) { continue; } if (transferStatus == TDCFileTransferDialogTransferStatusComplete) { return YES; } } return NO; } } return NO; // Default validation to NO. } - (void)clear:(id)sender { NSArray *stoppedFileTransfers = [self stoppedFileTransfers]; [stoppedFileTransfers makeObjectsPerformSelector:@selector(prepareForPermanentDestruction)]; [self.fileTransfersController removeObjects:stoppedFileTransfers]; [self updateClearButton]; } - (void)startTransferOfFile:(id)sender { NSString *savePath = self.downloadDestinationURLPrivate.path; NSMutableArray *fileTransfersPending = [NSMutableArray array]; /* Open all file transfers who are senders or have a path */ [self enumerateSelectedFileTransfers:^(TDCFileTransferDialogTransferController *fileTransfer, NSUInteger index, BOOL *stop) { TDCFileTransferDialogTransferStatus transferStatus = fileTransfer.transferStatus; if (transferStatus != TDCFileTransferDialogTransferStatusStopped && transferStatus != TDCFileTransferDialogTransferStatusRecoverableError) { return; } if (fileTransfer.isSender || fileTransfer.path != nil) { [fileTransfer open]; return; } else if (fileTransfer.path == nil && savePath != nil) { [fileTransfer openWithPath:savePath]; return; } [fileTransfersPending addObject:fileTransfer]; }]; /* If there are file transfers that weren't opened because of a missing path, then we now prompt the user for where they want to save the files. */ if (fileTransfersPending.count == 0) { return; } NSOpenPanel *openDialog = [NSOpenPanel openPanel]; openDialog.directoryURL = [TPCPathInfo userDownloadsURL]; openDialog.allowsMultipleSelection = NO; openDialog.canChooseDirectories = YES; openDialog.canChooseFiles = NO; openDialog.canCreateDirectories = YES; openDialog.resolvesAliases = YES; openDialog.message = TXTLS(@"TDCFileTransferDialog[dcm-w7]"); openDialog.prompt = TXTLS(@"Prompts[xne-79]"); [openDialog beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) { if (result != NSModalResponseOK) { return; } NSString *path = openDialog.URL.path; for (TDCFileTransferDialogTransferController *fileTransfer in fileTransfersPending) { [fileTransfer openWithPath:path]; } }]; } - (void)stopTransferOfFile:(id)sender { [self enumerateSelectedFileTransfers:^(TDCFileTransferDialogTransferController *fileTransfer, NSUInteger index, BOOL *stop) { [fileTransfer closeAndPostNotification:NO]; }]; } - (void)removeTransferFromList:(id)sender { NSArray *selectedFileTransfers = [self selectedFileTransfers]; for (TDCFileTransferDialogTransferController *fileTransfer in selectedFileTransfers) { [fileTransfer prepareForPermanentDestruction]; } [self.fileTransfersController removeObjects:selectedFileTransfers]; } - (void)openReceivedFile:(id)sender { [self enumerateSelectedFileTransfers:^(TDCFileTransferDialogTransferController *fileTransfer, NSUInteger index, BOOL *stop) { if (fileTransfer.isSender != NO) { return; } [RZWorkspace() openURL:fileTransfer.fileURL]; }]; } - (void)revealReceivedFileInFinder:(id)sender { [self enumerateSelectedFileTransfers:^(TDCFileTransferDialogTransferController *fileTransfer, NSUInteger index, BOOL *stop) { if (fileTransfer.isSender != NO) { return; } [RZWorkspace() selectFile:fileTransfer.filePath inFileViewerRootedAtPath:@""]; }]; } #pragma mark - #pragma mark Timer - (void)updateMaintenanceTimerOnMainThread { NSArray *activeFileTransfers = [self activeFileTransfers]; if (self.maintenanceTimer.timerIsActive) { if (activeFileTransfers.count == 0) { [self.maintenanceTimer stop]; } } else { if (activeFileTransfers.count > 0) { [self.maintenanceTimer start:1.0 onRepeat:YES]; } } } - (void)updateMaintenanceTimer { XRPerformBlockSynchronouslyOnMainQueue(^{ [self updateMaintenanceTimerOnMainThread]; }); } - (void)onMaintenanceTimer { NSArray *activeFileTransfers = [self activeFileTransfers]; for (TDCFileTransferDialogTransferController *fileTransfer in activeFileTransfers) { [fileTransfer onMaintenanceTimer]; } } #pragma mark - #pragma mark Table View Delegate - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { TDCFileTransferDialogTransferController *fileTransfer = self.fileTransfersController.arrangedObjects[row]; TDCFileTransferDialogTableCell *newView = (TDCFileTransferDialogTableCell *)[tableView makeViewWithIdentifier:@"GroupView" owner:self]; fileTransfer.transferTableCell = newView; return newView; } - (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row { TDCFileTransferDialogTableCell *tableCell = [tableView viewAtColumn:0 row:row makeIfNecessary:NO]; [tableCell prepareInitialState]; } #pragma mark - #pragma mark Network Information - (nullable NSString *)IPAddress { if ([TPCPreferences fileTransferIPAddressDetectionMethod] == TXFileTransferIPAddressMethodManual) { NSString *userAddress = [TPCPreferences fileTransferManuallyEnteredIPAddress]; if (userAddress.length == 0) { return nil; } return userAddress; } return self->_IPAddress; } - (void)clearIPAddress { self.IPAddress = nil; [self.IPAddressRequest cancelLookup]; self.IPAddressRequest = nil; } - (void)requestIPAddress { if (self.IPAddressRequest != nil) { return; } TLOInternetAddressLookup *lookupRequest = [[TLOInternetAddressLookup alloc] initWithDelegate:(id)self]; [lookupRequest performLookup]; self.IPAddressRequest = lookupRequest; } - (void)internetAddressLookupReturnedAddress:(NSString *)address { self.IPAddress = address; [self enumerateFileTransferSenders:^(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop) { if (fileTransfer.transferStatus != TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress) { return; } [fileTransfer noteIPAddressLookupSucceeded]; }]; self.IPAddressRequest = nil; } - (void)internetAddressLookupFailed { [self enumerateFileTransferSenders:^(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop) { if (fileTransfer.transferStatus != TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress) { return; } [fileTransfer noteIPAddressLookupFailed]; }]; self.IPAddressRequest = nil; } #pragma mark - #pragma mark Navigation - (TDCFileTransferDialogSelection)navigationSelection { return self.navigationControllerCell.selectedSegment; } - (void)navigationSelectionDidChange:(id)sender { TDCFileTransferDialogSelection selection = self.navigationSelection; NSPredicate *filterPredicate = nil; if (selection == TDCFileTransferDialogSelectionSending) { filterPredicate = [NSPredicate predicateWithFormat:@"isSender == YES"]; } else if (selection == TDCFileTransferDialogSelectionReceiving) { filterPredicate = [NSPredicate predicateWithFormat:@"isSender == NO"]; } self.fileTransfersController.filterPredicate = filterPredicate; } #pragma mark - #pragma mark Transfer Search - (NSUInteger)receiverCount { __block NSUInteger receiverCount = 0; [self enumerateFileTransferReceivers:^(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop) { receiverCount += 1; }]; return receiverCount; } - (NSArray *)stoppedFileTransfers { return [self fileTransfersMatchingCondition:^BOOL(TDCFileTransferDialogTransferController *fileTransfer) { TDCFileTransferDialogTransferStatus transferStatus = fileTransfer.transferStatus; if (transferStatus != TDCFileTransferDialogTransferStatusComplete && transferStatus != TDCFileTransferDialogTransferStatusStopped && transferStatus != TDCFileTransferDialogTransferStatusFatalError && transferStatus != TDCFileTransferDialogTransferStatusRecoverableError) { return NO; } return YES; }]; } - (NSArray *)activeFileTransfers { return [self fileTransfersMatchingCondition:^BOOL(TDCFileTransferDialogTransferController *fileTransfer) { TDCFileTransferDialogTransferStatus transferStatus = fileTransfer.transferStatus; if (transferStatus != TDCFileTransferDialogTransferStatusReceiving && transferStatus != TDCFileTransferDialogTransferStatusSending) { return NO; } return YES; }]; } - (NSArray *)fileTransfersMatchingCondition:(BOOL (NS_NOESCAPE ^)(TDCFileTransferDialogTransferController *fileTransfer))matchCondition { NSMutableArray *fileTransfers = [NSMutableArray array]; [self enumerateFileTransfers:^(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop) { if (matchCondition(fileTransfer) == NO) { return; } [fileTransfers addObject:fileTransfer]; }]; return [fileTransfers copy]; } - (nullable TDCFileTransferDialogTransferController *)fileTransferMatchingCondition:(BOOL (NS_NOESCAPE ^)(TDCFileTransferDialogTransferController *fileTransfer))matchCondition { __block TDCFileTransferDialogTransferController *fileTransferMatched = nil; [self enumerateFileTransfers:^(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop) { if (matchCondition(fileTransfer) == NO) { return; } fileTransferMatched = fileTransfer; *stop = YES; }]; return fileTransferMatched; } - (NSArray *)selectedFileTransfers { NSMutableArray *selectedFileTransfers = [NSMutableArray array]; [self enumerateSelectedFileTransfers:^(TDCFileTransferDialogTransferController *fileTransfer, NSUInteger index, BOOL *stop) { [selectedFileTransfers addObject:fileTransfer]; }]; return [selectedFileTransfers copy]; } - (NSArray *)fileTransfersMatchingClient:(IRCClient *)client { NSParameterAssert(client != nil); return [self fileTransfersMatchingCondition:^BOOL(TDCFileTransferDialogTransferController * _Nonnull fileTransfer) { return (fileTransfer.client == client); }]; } - (void)enumerateSelectedFileTransfers:(void (NS_NOESCAPE ^)(TDCFileTransferDialogTransferController *fileTransfer, NSUInteger index, BOOL *stop))enumerationBlock { NSIndexSet *selectedRows = self.fileTransferTable.selectedRowIndexes; [selectedRows enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { TDCFileTransferDialogTransferController *fileTransfer = self.fileTransfersController.arrangedObjects[index]; enumerationBlock(fileTransfer, index, stop); }]; } - (void)enumerateFileTransfers:(void (NS_NOESCAPE ^)(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop))enumerationBlock { [self _enumerateFileTransfers:enumerationBlock limitScope:NO limitScopeToSenders:NO]; } - (void)enumerateFileTransferReceivers:(void (NS_NOESCAPE ^)(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop))enumerationBlock { [self _enumerateFileTransfers:enumerationBlock limitScope:YES limitScopeToSenders:NO]; } - (void)enumerateFileTransferSenders:(void (NS_NOESCAPE ^)(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop))enumerationBlock { [self _enumerateFileTransfers:enumerationBlock limitScope:YES limitScopeToSenders:YES]; } - (void)_enumerateFileTransfers:(void (NS_NOESCAPE ^)(TDCFileTransferDialogTransferController *fileTransfer, BOOL *stop))enumerationBlock limitScope:(BOOL)limitScope limitScopeToSenders:(BOOL)limitScopeToSenders { NSParameterAssert(enumerationBlock != nil); for (TDCFileTransferDialogTransferController *fileTransfer in self.fileTransfersController.arrangedObjects) { if (limitScope && limitScopeToSenders != fileTransfer.isSender) { continue; } BOOL stop = NO; enumerationBlock(fileTransfer, &stop); if (stop) { break; } } } #pragma mark - #pragma mark Window Delegate - (void)windowWillClose:(NSNotification *)note { [self.window saveWindowStateForClass:self.class]; } - (void)hideWindow:(id)sender { [self close]; } @end #pragma mark - #pragma mark Destination Folder @implementation TDCFileTransferDialog (TDCFileTransferDialogDownloadDestinationExtension) - (nullable NSURL *)downloadDestinationURL { return self.downloadDestinationURLPrivate; } - (void)startUsingDownloadDestinationURL { NSData *bookmark = [RZUserDefaults() dataForKey:@"File Transfers -> File Transfer Download Folder Bookmark"]; if (bookmark == nil) { return; } BOOL resolvedBookmarkIsStale = YES; NSError *resolvedBookmarkError = nil; NSURL *resolvedBookmark = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&resolvedBookmarkIsStale error:&resolvedBookmarkError]; if (resolvedBookmark == nil) { LogToConsoleError("Error creating bookmark for URL: %{public}@", resolvedBookmarkError.localizedDescription); return; } self.downloadDestinationURLPrivate = resolvedBookmark; if ([self.downloadDestinationURLPrivate startAccessingSecurityScopedResource] == NO) { LogToConsoleError("Failed to access bookmark"); } } - (void)setDownloadDestinationURL:(nullable NSData *)downloadDestinationURL { if ( self.downloadDestinationURLPrivate) { [self.downloadDestinationURLPrivate stopAccessingSecurityScopedResource]; self.downloadDestinationURLPrivate = nil; } [RZUserDefaults() setObject:downloadDestinationURL forKey:@"File Transfers -> File Transfer Download Folder Bookmark"]; [self startUsingDownloadDestinationURL]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/File Transfers/TDCFileTransferDialogTableCell.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXGlobalModels.h" #import "TLOLocalization.h" #import "TDCFileTransferDialogTransferControllerPrivate.h" #import "TDCFileTransferDialogTableCellPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _filenameFieldWithProgressBarYCord 4 #define _filenameFieldWithoutProgressBarYCord 12 #define _transferInfoFieldWithProgressBarYCord 6 #define _transferInfoFieldWithoutProgressBarYCord 16 @interface TDCFileTransferDialogTableCell () @property (readonly) TDCFileTransferDialogTransferController *cellItem; @property (nonatomic, weak) IBOutlet NSProgressIndicator *progressIndicator; @property (nonatomic, weak) IBOutlet NSImageView *fileIconView; @property (nonatomic, weak) IBOutlet NSTextField *filenameTextField; @property (nonatomic, weak) IBOutlet NSTextField *filesizeTextField; @property (nonatomic, weak) IBOutlet NSTextField *transferProgressTextField; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *filenameTextFieldConstraint; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *transferProgressTextFieldConstraint; @property (readonly) BOOL isReceiving; @property (readonly) TDCFileTransferDialogTransferStatus transferStatus; @property (readonly) uint64_t processedFilesize; @property (readonly) uint64_t totalFilesize; @property (readonly) uint64_t currentRecord; @property (readonly, copy) NSArray *speedRecords; @property (readonly, copy, nullable) NSString *errorMessageDescription; @property (readonly, copy, nullable) NSString *path; @property (readonly, copy) NSString *filename; @property (readonly, copy, nullable) NSString *filePath; @property (readonly, copy) NSString *hostAddress; @property (readonly, copy) NSString *peerNickname; @property (readonly) uint16_t hostPort; @end @implementation TDCFileTransferDialogTableCell #pragma mark - #pragma mark Status Information - (void)prepareInitialState { NSString *filename = self.filename; self.filenameTextField.stringValue = filename; uint64_t totalFilesize = self.totalFilesize; NSString *totalFilesizeString = [NSByteCountFormatter stringFromByteCountWithPaddedDigits:totalFilesize]; self.filesizeTextField.stringValue = totalFilesizeString; self.progressIndicator.doubleValue = 0; self.progressIndicator.minValue = 0; self.progressIndicator.maxValue = totalFilesize; NSImage *iconImage = [RZWorkspace() iconForFileType:filename.pathExtension]; self.fileIconView.image = iconImage; [self reloadStatusInformation]; } - (void)reloadStatusInformation { XRPerformBlockSynchronouslyOnMainQueue(^{ [self _reloadStatusInformation]; }); } - (void)_reloadStatusInformation { TDCFileTransferDialogTransferStatus transferStatus = self.transferStatus; BOOL transferIsStopped = (transferStatus == TDCFileTransferDialogTransferStatusComplete || transferStatus == TDCFileTransferDialogTransferStatusFatalError || transferStatus == TDCFileTransferDialogTransferStatusRecoverableError || transferStatus == TDCFileTransferDialogTransferStatusStopped || transferStatus == TDCFileTransferDialogTransferStatusIsListeningAsSender || transferStatus == TDCFileTransferDialogTransferStatusIsListeningAsReceiver || transferStatus == TDCFileTransferDialogTransferStatusInitializing || transferStatus == TDCFileTransferDialogTransferStatusMappingListeningPort || transferStatus == TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress || transferStatus == TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept || transferStatus == TDCFileTransferDialogTransferStatusWaitingForResumeAccept); uint64_t processedFilesize = self.processedFilesize; if (transferIsStopped) { if (self.progressIndicator.hidden == NO) { self.progressIndicator.hidden = YES; self.filenameTextFieldConstraint.constant = _filenameFieldWithoutProgressBarYCord; self.transferProgressTextFieldConstraint.constant = _transferInfoFieldWithoutProgressBarYCord; [self layoutSubtreeIfNeeded]; } } else { if (self.progressIndicator.hidden) { self.progressIndicator.hidden = NO; self.filenameTextFieldConstraint.constant = _filenameFieldWithProgressBarYCord; self.transferProgressTextFieldConstraint.constant = _transferInfoFieldWithProgressBarYCord; [self layoutSubtreeIfNeeded]; } } if (transferIsStopped == NO) { if (transferStatus == TDCFileTransferDialogTransferStatusConnecting) { self.progressIndicator.indeterminate = YES; [self.progressIndicator startAnimation:nil]; } else { self.progressIndicator.indeterminate = NO; self.progressIndicator.doubleValue = self.processedFilesize; } } switch (transferStatus) { case TDCFileTransferDialogTransferStatusStopped: { if (self.isReceiving) { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[jvh-u7]", self.peerNickname); } else { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[w3h-p8]", self.peerNickname); } break; } case TDCFileTransferDialogTransferStatusMappingListeningPort: { if (self.isReceiving) { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[495-90]", self.peerNickname); } else { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[j1z-88]", self.peerNickname); } break; } case TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress: { if (self.isReceiving) { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[6t1-mb]", self.peerNickname); } else { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[onl-av]", self.peerNickname); } break; } case TDCFileTransferDialogTransferStatusInitializing: { if (self.isReceiving) { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[42z-mg]", self.peerNickname); } else { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[pcv-kg]", self.peerNickname); } break; } case TDCFileTransferDialogTransferStatusIsListeningAsSender: { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[ca5-2v]", self.peerNickname); break; } case TDCFileTransferDialogTransferStatusIsListeningAsReceiver: { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[pip-z6]", self.peerNickname); break; } case TDCFileTransferDialogTransferStatusFatalError: case TDCFileTransferDialogTransferStatusRecoverableError: { self.transferProgressTextField.stringValue = self.errorMessageDescription; break; } case TDCFileTransferDialogTransferStatusComplete: { if (self.isReceiving) { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[6gu-za]", self.peerNickname); } else { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[rx7-xy]", self.peerNickname); } break; } case TDCFileTransferDialogTransferStatusSending: case TDCFileTransferDialogTransferStatusReceiving: { /* Format time remaining */ NSTimeInterval timeRemaining = 0; NSString *timeRemainingString = nil; uint64_t currentSpeed = self.currentSpeed; if (currentSpeed > 0) { timeRemaining = ((self.totalFilesize - processedFilesize) / currentSpeed); if (timeRemaining > 0) { timeRemainingString = TXHumanReadableTimeInterval(timeRemaining, YES, (NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond)); } } /* Update status */ NSString *totalFilesizeString = self.filesizeTextField.stringValue; NSString *currentSpeedString = [NSByteCountFormatter stringFromByteCountWithPaddedDigits:currentSpeed]; NSString *processedFilesizeString = [NSByteCountFormatter stringFromByteCountWithPaddedDigits:processedFilesize]; NSString *statusString = nil; if (self.isReceiving) { if (timeRemainingString) { statusString = TXTLS(@"TDCFileTransferDialog[9xn-7j]", processedFilesizeString, totalFilesizeString, currentSpeedString, self.peerNickname, timeRemainingString); } else { statusString = TXTLS(@"TDCFileTransferDialog[7dk-lp]", processedFilesizeString, totalFilesizeString, currentSpeedString, self.peerNickname); } } else { if (timeRemainingString) { statusString = TXTLS(@"TDCFileTransferDialog[u17-ql]", processedFilesizeString, totalFilesizeString, currentSpeedString, self.peerNickname, timeRemainingString); } else { statusString = TXTLS(@"TDCFileTransferDialog[nvm-nd]", processedFilesizeString, totalFilesizeString, currentSpeedString, self.peerNickname); } } self.transferProgressTextField.stringValue = statusString; break; } case TDCFileTransferDialogTransferStatusConnecting: { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[7nf-fr]", self.peerNickname); break; } case TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept: { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[cku-24]", self.peerNickname); break; } case TDCFileTransferDialogTransferStatusWaitingForResumeAccept: { self.transferProgressTextField.stringValue = TXTLS(@"TDCFileTransferDialog[gxq-zu]", self.peerNickname); break; } } /* Update clear button */ [self updateClearButton]; } #pragma mark - #pragma mark Proxy Methods - (void)updateClearButton { [self.cellItem updateClearButton]; } - (void)onMaintenanceTimer { [self.cellItem onMaintenanceTimer]; } #pragma mark - #pragma mark Properties - (TDCFileTransferDialogTransferController *)cellItem { return self.objectValue; } - (TDCFileTransferDialogTransferStatus)transferStatus { return self.cellItem.transferStatus; } - (BOOL)isReceiving { return (self.cellItem.isSender == NO); } - (nullable NSString *)path { return self.cellItem.path; } - (NSString *)filename { return self.cellItem.filename; } - (nullable NSString *)filePath { return self.cellItem.filePath; } - (NSString *)peerNickname { return self.cellItem.peerNickname; } - (nullable NSString *)errorMessageDescription { return self.cellItem.errorMessageDescription; } - (NSString *)hostAddress { return self.cellItem.hostAddress; } - (uint16_t)hostPort { return self.cellItem.hostPort; } - (uint64_t)totalFilesize { return self.cellItem.totalFilesize; } - (uint64_t)processedFilesize { return self.cellItem.processedFilesize; } - (uint64_t)currentRecord { return self.cellItem.currentRecord; } - (uint64_t)currentSpeed { NSArray *speedRecords = self.speedRecords; if (speedRecords.count == 0) { return 0; } uint64_t totalTransferred = 0; for (NSNumber *record in speedRecords) { totalTransferred += record.unsignedLongLongValue; } return (totalTransferred / speedRecords.count); } - (NSArray *)speedRecords { return self.cellItem.speedRecords; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/File Transfers/TDCFileTransferDialogTransferController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "GCDAsyncSocket.h" #import "TXGlobalModels.h" #import "IRCClientPrivate.h" #import "TPCPathInfo.h" #import "TPCPreferencesLocal.h" #import "TLOEncryptionManagerPrivate.h" #import "TLOLocalization.h" #import "TDCFileTransferDialogTableCellPrivate.h" #import "TDCFileTransferDialogTransferControllerPrivate.h" #import "TDCFileTransferDialogInternal.h" NS_ASSUME_NONNULL_BEGIN #define RECORDS_LENGTH 10 #define MAX_QUEUE_SIZE 2 #define BUFFER_SIZE (1024 * 64) #define RATE_LIMIT (1024 * 1024 * 10) #define _connectTimeout 30.0 #define _sendDataTimeout 30.0 #define _resumeAcceptTimeout 10.0 @interface TDCFileTransferDialogTransferController () @property (nonatomic, strong, readwrite) IRCClient *client; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, assign, readwrite) BOOL isResume; @property (nonatomic, assign, readwrite) BOOL isReversed; @property (nonatomic, assign, readwrite) BOOL isSender; @property (nonatomic, assign, readwrite) TDCFileTransferDialogTransferStatus transferStatus; @property (nonatomic, assign, readwrite) uint64_t totalFilesize; @property (nonatomic, assign, readwrite) uint64_t processedFilesize; @property (nonatomic, assign, readwrite) uint64_t currentRecord; @property (nonatomic, strong) NSMutableArray *speedRecordsPrivate; @property (nonatomic, copy, readwrite, nullable) NSString *errorMessageDescription; @property (nonatomic, copy, readwrite, nullable) NSString *path; @property (nonatomic, copy, readwrite) NSString *filename; @property (nonatomic, copy, readwrite) NSString *hostAddress; @property (nonatomic, copy, readwrite) NSString *peerNickname; @property (nonatomic, copy, readwrite, nullable) NSString *transferToken; @property (nonatomic, copy, readwrite) NSString *uniqueIdentifier; @property (nonatomic, assign, readwrite) uint16_t hostPort; @property (nonatomic, strong, nullable) NSFileHandle *fileHandle; @property (nonatomic, strong, nullable) XRPortMapper *portMapping; @property (nonatomic, assign) NSUInteger sendQueueSize; @property (nonatomic, strong, nullable) dispatch_queue_t serverDispatchQueue; @property (nonatomic, strong, nullable) dispatch_queue_t serverSocketQueue; @property (nonatomic, strong, nullable) GCDAsyncSocket *listeningServer; @property (nonatomic, strong, nullable) GCDAsyncSocket *listeningServerConnectedClient; @property (nonatomic, strong, nullable) GCDAsyncSocket *connectionToRemoteServer; @property (nonatomic, strong, nullable) id transferProgressHandler; // Used to prevent system sleep @property (readonly) uint64_t currentFilesize; @property (readonly) TDCFileTransferDialog *transferDialog; @property (readonly, nullable) GCDAsyncSocket *readSocket; @property (readonly, nullable) GCDAsyncSocket *writeSocket; @end @implementation TDCFileTransferDialogTransferController #pragma mark - #pragma mark Initialization + (nullable instancetype)receiverForClient:(IRCClient *)client nickname:(NSString *)nickname address:(NSString *)hostAddress port:(uint16_t)hostPort filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken { NSParameterAssert(client != nil); NSParameterAssert(nickname != nil); NSParameterAssert(hostAddress != nil); NSParameterAssert(filename != nil); /* Construct controller */ TDCFileTransferDialogTransferController *controller = [[self alloc] initWithClient:client]; if (transferToken.length > 0) { controller.transferToken = transferToken; controller.isReversed = YES; } controller.isSender = NO; controller.peerNickname = nickname; controller.hostAddress = hostAddress; controller.hostPort = hostPort; controller.filename = filename; controller.totalFilesize = totalFilesize; return controller; } + (nullable instancetype)senderForClient:(IRCClient *)client nickname:(NSString *)nickname path:(NSString *)path { NSParameterAssert(client != nil); NSParameterAssert(nickname != nil); NSParameterAssert(path != nil); NSString *filename = path.lastPathComponent; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 if ([TPCPreferences textEncryptionIsEnabled]) { /* Ask whether we should be allowed to add the file. */ BOOL allowWithOTR = [sharedEncryptionManager() safeToTransferFile:filename to:[client encryptionAccountNameForUser:nickname] from:[client encryptionAccountNameForLocalUser] isIncomingFileTransfer:NO]; if (allowWithOTR == NO) { return nil; // This operation is not allowed... } } #endif /* Gather file information */ NSDictionary *fileAttributes = [RZFileManager() attributesOfItemAtPath:path error:NULL]; if (fileAttributes == nil) { return nil; } uint64_t totalFilesize = [fileAttributes fileSize]; if (totalFilesize == 0) { LogToConsoleError("Fatal error: Cannot create sender because filesize == 0"); return nil; } NSString *filePath = path.stringByDeletingLastPathComponent; /* Construct controller */ TDCFileTransferDialogTransferController *controller = [[self alloc] initWithClient:client]; controller.isReversed = [TPCPreferences fileTransferRequestsAreReversed]; controller.isSender = YES; controller.peerNickname = nickname; controller.path = filePath; controller.filename = filename; controller.totalFilesize = totalFilesize; return controller; } - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithClient:(IRCClient *)client { NSParameterAssert(client != nil); if ((self = [super init])) { self.client = client; self.clientId = client.uniqueIdentifier; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.speedRecordsPrivate = [NSMutableArray array]; self.transferStatus = TDCFileTransferDialogTransferStatusStopped; self.uniqueIdentifier = [NSString stringWithUUID]; [RZNotificationCenter() addObserver:self selector:@selector(clientDisconnected:) name:IRCClientDidDisconnectNotification object:self.client]; [RZNotificationCenter() addObserver:self selector:@selector(peerNicknameChanged:) name:IRCClientUserNicknameChangedNotification object:self.client]; } - (void)prepareForPermanentDestruction { self.transferTableCell = nil; [self closeAndPostNotification:NO]; [RZNotificationCenter() removeObserver:self]; } #pragma mark - #pragma mark Error Handling - (void)failWithNoSpaceLeftOnDevice { [self closeWithLocalizedError:TXTLS(@"TDCFileTransferDialog[79f-s0]")]; } - (void)closeWithLocalizedError:(NSString *)errorLocalization { [self closeWithLocalizedError:errorLocalization description:nil isFatalError:NO]; } - (void)closeWithLocalizedError:(NSString *)errorLocalization isFatalError:(BOOL)isFatalError { [self closeWithLocalizedError:errorLocalization description:nil isFatalError:isFatalError]; } - (void)closeWithLocalizedError:(NSString *)errorLocalization description:(nullable NSString *)errorDescription { [self closeWithLocalizedError:errorLocalization description:errorDescription isFatalError:NO]; } - (void)closeWithLocalizedError:(NSString *)errorLocalization description:(nullable NSString *)errorDescription isFatalError:(BOOL)isFatalError { NSParameterAssert(errorLocalization != nil); if (errorDescription == nil) { self.errorMessageDescription = TXTLS(errorLocalization, self.peerNickname); } else { self.errorMessageDescription = TXTLS(errorLocalization, self.peerNickname, errorDescription); } if (isFatalError) { self.transferStatus = TDCFileTransferDialogTransferStatusFatalError; } else { self.transferStatus = TDCFileTransferDialogTransferStatusRecoverableError; } [self close]; } #pragma mark - #pragma mark Dispatch Queue Management - (void)createDispatchQueues { NSString *uniqueId = [NSString stringWithUUID]; NSString *dispatchQueueName = [NSString stringWithFormat:@"Textual.TDCFileTransferDialogTransferController.DCC-SocketDispatchQueue-%@", uniqueId]; self.serverDispatchQueue = XRCreateDispatchQueueWithPriority(dispatchQueueName.UTF8String, DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT); NSString *socketQueueName = [NSString stringWithFormat:@"Textual.TDCFileTransferDialogTransferController.DCC-SocketReadWriteQueue-%@", uniqueId]; self.serverSocketQueue = XRCreateDispatchQueueWithPriority(socketQueueName.UTF8String, DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT); } - (void)destroyDispatchQueues { self.serverDispatchQueue = nil; self.serverSocketQueue = nil; } #pragma mark - #pragma mark Opening/Closing Transfer - (void)disableSystemSleep { self.transferProgressHandler = [RZProcessInfo() beginActivityWithOptions:NSActivityUserInitiated reason:@"Transferring file"]; } - (void)enableSystemSleep { if (self.transferProgressHandler == nil) { return; } [RZProcessInfo() endActivity:self.transferProgressHandler]; self.transferProgressHandler = nil; } - (BOOL)receiveUnencryptedFile { #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 if ([TPCPreferences textEncryptionIsEnabled]) { BOOL allowWithOTR = [sharedEncryptionManager() safeToTransferFile:self.filename to:[self.client encryptionAccountNameForUser:self.peerNickname] from:[self.client encryptionAccountNameForLocalUser] isIncomingFileTransfer:YES]; if (allowWithOTR == NO) { return NO; // This operation is not allowed... } } #endif return YES; } - (void)open { [self openWithPath:nil]; } - (void)openWithPathOrUserDownloads { NSString *path = nil; if (self.path == nil) { path = [TPCPathInfo userDownloads]; } [self openWithPath:path]; } - (void)openWithPath:(nullable NSString *)path { if (self.path == nil) { self.path = path; } if (self.client.isLoggedIn == NO) { [self _closeWithClientDisconnectedError]; return; } if (self.isSender) { [self openTransfer]; } else { if ([self receiveUnencryptedFile] == NO) { return; } [self sendTransferResumeRequestToClient]; } } - (void)openTransfer { if (self.isSender) { if (self.isReversed) { [self updateIPAddress]; } else { [self openConnectionAsServer]; } } else { if (self.isReversed) { [self openConnectionAsServer]; } else { [self openConnectionToHost]; } } } - (void)openConnectionToHost { [self closeAndPostNotification:NO]; [self resetProperties]; [self createDispatchQueues]; self.transferStatus = TDCFileTransferDialogTransferStatusConnecting; GCDAsyncSocket *connectionToRemoteServer = [[GCDAsyncSocket alloc] initWithDelegate:(id)self delegateQueue:self.serverDispatchQueue socketQueue:self.serverSocketQueue]; NSError *connectionError = nil; BOOL isConnected = NO; /* Use the interface of the configured IP address instead of the default. */ /* Default interface is used if IP address is not found locally. */ NSString *networkInterface = [self networkInterfaceMatchingAddress]; if (networkInterface) { isConnected = [connectionToRemoteServer connectToHost:self.hostAddress onPort:self.hostPort viaInterface:networkInterface withTimeout:_connectTimeout error:&connectionError]; } else { isConnected = [connectionToRemoteServer connectToHost:self.hostAddress onPort:self.hostPort withTimeout:_connectTimeout error:&connectionError]; } if (isConnected == NO) { if (connectionError) { LogToConsoleError("DCC Connect Error: %{public}@", connectionError.localizedDescription); } [self closeWithLocalizedError:@"TDCFileTransferDialog[fn8-sx]"]; return; } self.connectionToRemoteServer = connectionToRemoteServer; [self disableSystemSleep]; } - (void)openConnectionAsServer { [self closeAndPostNotification:NO]; [self resetProperties]; [self createDispatchQueues]; self.transferStatus = TDCFileTransferDialogTransferStatusInitializing; self.hostPort = [TPCPreferences fileTransferPortRangeStart]; while ([self tryToOpenConnectionAsServer] == NO) { self.hostPort += 1; if (self.hostPort > [TPCPreferences fileTransferPortRangeEnd]) { [self closeWithLocalizedError:@"TDCFileTransferDialog[vxc-sd]"]; return; } } [self disableSystemSleep]; } - (BOOL)tryToOpenConnectionAsServer { GCDAsyncSocket *listeningServer = [[GCDAsyncSocket alloc] initWithDelegate:(id)self delegateQueue:self.serverDispatchQueue socketQueue:self.serverSocketQueue]; BOOL isActive = [listeningServer acceptOnPort:self.hostPort error:NULL]; if (isActive == NO) { return NO; } self.listeningServer = listeningServer; /* Try to map the port */ XRPortMapper *portMapping = [[XRPortMapper alloc] initWithPort:self.hostPort]; portMapping.mapTCP = YES; portMapping.mapUDP = NO; portMapping.desiredPublicPort = self.hostPort; self.portMapping = portMapping; [RZNotificationCenter() addObserver:self selector:@selector(portMapperDidFinishWork:) name:XRPortMapperDidChangedNotification object:self.portMapping]; self.transferStatus = TDCFileTransferDialogTransferStatusMappingListeningPort; if ([self.portMapping open] == NO) { [self portMapperDidFinishWork:nil]; } return YES; } - (nullable NSString *)networkInterfaceMatchingAddress { return [TPCPreferences fileTransferIPAddressInterfaceName]; } - (void)portMapperDidFinishWork:(NSNotification *)aNotification { NSAssertReturn(self.transferStatus == TDCFileTransferDialogTransferStatusMappingListeningPort); if (self.portMapping.isMapped) { [self updateIPAddress]; LogToConsoleInfo("Successful port mapping on port %{public}hu", self.hostPort); return; } LogToConsoleError("Port mapping failed with error code: %{public}i", self.portMapping.error); if (self.isReversed) { [self closeWithLocalizedError:@"TDCFileTransferDialog[vxc-sd]"]; return; } /* If mapping fails, we silently fail */ /* We tried and it was successful, then that is good, but if we did not, still start listening just incase other conditions allow the transfer to still take place. */ [self updateIPAddress]; } - (void)updateIPAddress { LogStackTraceWithType(LogToConsoleTypeDebug); NSString *address = self.transferDialog.IPAddress; LogToConsoleDebug("TDCFileTransferDialog cached IP address: %{private}@", address); TXFileTransferIPAddressMethodDetection detectionMethod = [TPCPreferences fileTransferIPAddressDetectionMethod]; BOOL manuallyDetect = (detectionMethod == TXFileTransferIPAddressMethodManual); if (address == nil && manuallyDetect == NO) { NSString *publicAddress = self.portMapping.publicAddress; LogToConsoleDebug("Port mapper public IP address: %{private}@", publicAddress); if (publicAddress.isIPAddress) { self.transferDialog.IPAddress = publicAddress; address = publicAddress; } } /* Request address? */ if (address == nil) { if (manuallyDetect || detectionMethod == TXFileTransferIPAddressMethodRouterOnly) { LogToConsoleError("User has set IP address detection to be manual but have no address set"); [self noteIPAddressLookupFailed]; } else { LogToConsoleDebug("Performing IP address lookup using the Internet"); [self.transferDialog requestIPAddress]; self.transferStatus = TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress; } return; } [self noteIPAddressLookupSucceeded]; } - (void)closePortMapping { if (self.portMapping == nil) { return; } [RZNotificationCenter() removeObserver:self name:XRPortMapperDidChangedNotification object:self.portMapping]; XRPerformBlockSynchronouslyOnMainQueue(^{ [self.portMapping close]; self.portMapping = nil; }); } - (void)noteIPAddressLookupSucceeded { LogStackTraceWithType(LogToConsoleTypeDebug); if (self.isSender) { if (self.isReversed) { self.transferStatus = TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept; } else { self.transferStatus = TDCFileTransferDialogTransferStatusIsListeningAsSender; } } else { if (self.isReversed) { self.transferStatus = TDCFileTransferDialogTransferStatusIsListeningAsReceiver; } else { return; } } [self sendTransferRequestToClient]; } - (void)noteIPAddressLookupFailed { [self closeWithLocalizedError:@"TDCFileTransferDialog[47s-1s]"]; } - (void)didReceiveResumeRequest:(uint64_t)proposedPosition { uint64_t currentFilesize = self.currentFilesize; if (proposedPosition == 0 || currentFilesize < proposedPosition) { return; } self.isResume = YES; self.processedFilesize = proposedPosition; [self sendTransferResumeAcceptToClient]; } - (void)didReceiveResumeAccept:(uint64_t)proposedPosition { [self cancelPerformRequestsWithSelector:@selector(transferResumeRequestTimeout) object:nil]; uint64_t currentFilesize = self.currentFilesize; if (currentFilesize != proposedPosition) { [self closeWithLocalizedError:@"TDCFileTransferDialog[0ov-tr]" isFatalError:YES]; return; } self.isResume = YES; self.processedFilesize = currentFilesize; [self openTransfer]; } - (void)didReceiveSendRequest:(NSString *)hostAddress hostPort:(uint16_t)hostPort { NSParameterAssert(hostAddress != nil); self.hostAddress = hostAddress; self.hostPort = hostPort; self.processedFilesize = 0; [self openConnectionToHost]; } - (void)buildTransferToken { NSUInteger loopedCount = 0; do { NSString *transferToken = [NSString stringWithUnsignedInteger:TXRandomNumber(9999)]; BOOL transferExists = [self.transferDialog fileTransferExistsWithToken:transferToken]; if (transferExists == NO) { self.transferToken = transferToken; break; } loopedCount += 1; } while (loopedCount < 300); } - (void)sendTransferRequestToClient { if (self.isSender) { uint64_t currentFilesize = self.currentFilesize; if (self.isReversed) { [self buildTransferToken]; [self.client sendFile:self.peerNickname port:0 filename:self.filename filesize:currentFilesize token:self.transferToken]; } else { [self.client sendFile:self.peerNickname port:self.hostPort filename:self.filename filesize:currentFilesize token:nil]; } } else { if (self.isReversed) { [self.client sendFile:self.peerNickname port:self.hostPort filename:self.filename filesize:self.totalFilesize token:self.transferToken]; } } } - (void)sendTransferResumeRequestToClient { uint64_t currentFilesize = self.currentFilesize; if (currentFilesize == 0 || currentFilesize > self.totalFilesize) { [self transferResumeRequestTimeout]; return; } [self performSelectorInCommonModes:@selector(transferResumeRequestTimeout) withObject:nil afterDelay:_resumeAcceptTimeout]; self.transferStatus = TDCFileTransferDialogTransferStatusWaitingForResumeAccept; if (self.isReversed) { [self.client sendFileResume:self.peerNickname port:0 filename:self.filename filesize:currentFilesize token:self.transferToken]; } else { [self.client sendFileResume:self.peerNickname port:self.hostPort filename:self.filename filesize:currentFilesize token:nil]; } } - (void)sendTransferResumeAcceptToClient { if (self.isReversed) { [self.client sendFileResumeAccept:self.peerNickname port:0 filename:self.filename filesize:self.processedFilesize token:self.transferToken]; } else { [self.client sendFileResumeAccept:self.peerNickname port:self.hostPort filename:self.filename filesize:self.processedFilesize token:nil]; } } - (void)transferResumeRequestTimeout { [self openTransfer]; } - (void)peerNicknameChanged:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; NSString *oldNickname = userInfo[@"oldNickname"]; if ([self.peerNickname isEqualToString:oldNickname] == NO) { return; } self.peerNickname = userInfo[@"newNickname"]; } - (void)clientDisconnected:(NSNotification *)notification { [self closeWithClientDisconnectedError]; } - (void)closeWithClientDisconnectedError { /* If the controller is already sending or receiving data, then a connection is already established to the peer which can function without a connection to IRC. If data is not being transferred then fail immediately. */ TDCFileTransferDialogTransferStatus transferStatus = self.transferStatus; if (transferStatus != TDCFileTransferDialogTransferStatusConnecting && transferStatus != TDCFileTransferDialogTransferStatusInitializing && transferStatus != TDCFileTransferDialogTransferStatusIsListeningAsReceiver && transferStatus != TDCFileTransferDialogTransferStatusIsListeningAsSender && transferStatus != TDCFileTransferDialogTransferStatusMappingListeningPort && transferStatus != TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress && transferStatus != TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept && transferStatus != TDCFileTransferDialogTransferStatusWaitingForResumeAccept) { return; } [self _closeWithClientDisconnectedError]; } - (void)_closeWithClientDisconnectedError { [self closeWithLocalizedError:@"TDCFileTransferDialog[12p-0v]" isFatalError:NO]; } - (void)close { [self closeAndPostNotification:YES]; } - (void)closeAndPostNotification:(BOOL)postNotification { [self cancelPerformRequests]; if ( self.listeningServer) { [self.listeningServer disconnect]; self.listeningServer = nil; } if ( self.listeningServerConnectedClient) { [self.listeningServerConnectedClient disconnect]; self.listeningServerConnectedClient = nil; } if ( self.connectionToRemoteServer) { [self.connectionToRemoteServer disconnect]; self.connectionToRemoteServer = nil; } [self destroyDispatchQueues]; [self closePortMapping]; [self closeFileHandle]; if (self.transferStatus != TDCFileTransferDialogTransferStatusComplete && self.transferStatus != TDCFileTransferDialogTransferStatusFatalError && self.transferStatus != TDCFileTransferDialogTransferStatusRecoverableError) { self.transferStatus = TDCFileTransferDialogTransferStatusStopped; } if (postNotification) { if (self.transferStatus == TDCFileTransferDialogTransferStatusFatalError || self.transferStatus == TDCFileTransferDialogTransferStatusRecoverableError) { if (self.isSender) { [self.client notifyFileTransfer:TXNotificationTypeFileTransferSendFailed nickname:self.peerNickname filename:self.filename filesize:self.totalFilesize requestIdentifier:self.uniqueIdentifier]; } else { [self.client notifyFileTransfer:TXNotificationTypeFileTransferReceiveFailed nickname:self.peerNickname filename:self.filename filesize:self.totalFilesize requestIdentifier:self.uniqueIdentifier]; } } else if (self.transferStatus == TDCFileTransferDialogTransferStatusComplete) { if (self.isSender) { [self.client notifyFileTransfer:TXNotificationTypeFileTransferSendSuccessful nickname:self.peerNickname filename:self.filename filesize:self.totalFilesize requestIdentifier:self.uniqueIdentifier]; } else { [self.client notifyFileTransfer:TXNotificationTypeFileTransferReceiveSuccessful nickname:self.peerNickname filename:self.filename filesize:self.totalFilesize requestIdentifier:self.uniqueIdentifier]; } } } [self.transferDialog updateMaintenanceTimer]; [self enableSystemSleep]; } #pragma mark - #pragma mark Timer - (void)onMaintenanceTimer { NSAssertReturn(self.transferStatus == TDCFileTransferDialogTransferStatusReceiving || self.transferStatus == TDCFileTransferDialogTransferStatusSending); XRPerformBlockSynchronouslyOnQueue(self.serverDispatchQueue, ^{ @synchronized(self.speedRecords) { [self.speedRecordsPrivate addObject:@(self.currentRecord)]; if (self.speedRecordsPrivate.count > RECORDS_LENGTH) { [self.speedRecordsPrivate removeObjectAtIndex:0]; } } self.currentRecord = 0; [self reloadStatusInformation]; }); [self send]; } #pragma mark - #pragma mark File Handle - (void)setNonexistentFilename { NSString *filePath = self.filePath; if ([RZFileManager() fileExistsAtPath:filePath] == NO) { return; } NSString *filenameExtension = self.filename.pathExtension; NSString *filenameWithoutExtension = self.filename.stringByDeletingPathExtension; NSUInteger i = 1; do { NSString *newFilename = nil; if (filenameExtension.length == 0) { newFilename = [NSString stringWithFormat:@"%@_%lu", filenameWithoutExtension, i]; } else { newFilename = [NSString stringWithFormat:@"%@_%lu.%@", filenameWithoutExtension, i, filenameExtension]; } filePath = [self.path stringByAppendingPathComponent:newFilename]; i += 1; } while ([RZFileManager() fileExistsAtPath:filePath]); self.filename = filePath.lastPathComponent; } - (BOOL)openFileHandle { NSString *filePath = self.filePath; if (self.isSender == NO && self.isResume == NO) { [self setNonexistentFilename]; [RZFileManager() createFileAtPath:filePath contents:[NSData data] attributes:nil]; } NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath]; if (fileHandle == nil) { [self closeWithLocalizedError:@"TDCFileTransferDialog[nab-dx]"]; return NO; } if (self.isResume) { [fileHandle seekToFileOffset:self.processedFilesize]; } self.fileHandle = fileHandle; return YES; } - (void)closeFileHandle { if (self.fileHandle == nil) { return; } [self.fileHandle closeFile]; self.fileHandle = nil; } #pragma mark - #pragma mark Socket Delegate - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { if (self.isActingAsServer == NO) { return; } if (self.listeningServerConnectedClient == nil) { self.listeningServerConnectedClient = newSocket; } else { [newSocket disconnect]; return; } if (self.isReversed) { self.transferStatus = TDCFileTransferDialogTransferStatusReceiving; } else { self.transferStatus = TDCFileTransferDialogTransferStatusSending; } [self.transferDialog updateMaintenanceTimer]; if ([self openFileHandle] == NO) { return; } if (self.isReversed) { [self.readSocket readDataWithTimeout:(-1) tag:0]; } else { [self send]; } } - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { if (self.isActingAsClient == NO) { return; } if (self.isReversed == NO) { self.transferStatus = TDCFileTransferDialogTransferStatusReceiving; } else { self.transferStatus = TDCFileTransferDialogTransferStatusSending; } [self.transferDialog updateMaintenanceTimer]; if ([self openFileHandle] == NO) { return; } if (self.isReversed == NO) { [self.readSocket readDataWithTimeout:(-1) tag:0]; } else { [self send]; } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error { if (self.transferStatus == TDCFileTransferDialogTransferStatusComplete || self.transferStatus == TDCFileTransferDialogTransferStatusFatalError || self.transferStatus == TDCFileTransferDialogTransferStatusRecoverableError) { return; } if (error) { [self closeWithLocalizedError:@"TDCFileTransferDialog[s79-3a]" description:error.localizedDescription]; } else { [self close]; } } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { if (self.isSender != NO) { return; } /* Update progress */ self.currentRecord += data.length; self.processedFilesize += data.length; if (data.length > 0) { @try { [self.fileHandle writeData:data]; } @catch (NSException *exception) { LogToConsoleError("Caught exception: %{public}@", exception.reason); LogStackTrace(); if ([exception.reason contains:@"No space left on device"]) { [self failWithNoSpaceLeftOnDevice]; return; } [self closeWithLocalizedError:TXTLS(@"TDCFileTransferDialog[05g-c8]")]; return; } // @catch } /* Send acknowledgement back to server */ uint32_t processedFilesize = (self.processedFilesize & 0xFFFFFFFF); unsigned char ackPacket[4]; ackPacket[0] = ((processedFilesize >> 24) & 0xFF); ackPacket[1] = ((processedFilesize >> 16) & 0xFF); ackPacket[2] = ((processedFilesize >> 8) & 0xFF); ackPacket[3] = (processedFilesize & 0xFF); NSData *ackPacketData = [NSData dataWithBytes:ackPacket length:4]; [self.readSocket writeData:ackPacketData withTimeout:(-1) tag:0]; /* Continue requesting data if the transfer is not complete */ if (self.processedFilesize < self.totalFilesize) { [self.readSocket readDataWithTimeout:(-1) tag:0]; return; } /* Update status and tear down transfer */ self.transferStatus = TDCFileTransferDialogTransferStatusComplete; [self close]; } #pragma mark - #pragma mark Socket Write - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag { if (self.isSender == NO) { return; } /* Acknowledge sent data */ self.sendQueueSize -= 1; /* Continue sending data if the transfer is not complete */ if (self.processedFilesize < self.totalFilesize) { [self send]; return; } /* Wait until the send queue has cleared before closing connection. Just because we finished processing the data here doesn't mean it has been sent yet. */ if (self.sendQueueSize > 0) { return; } /* Update status and tear down transfer */ self.transferStatus = TDCFileTransferDialogTransferStatusComplete; [self close]; } - (void)send { if (self.isSender == NO) { return; } if (self.transferStatus != TDCFileTransferDialogTransferStatusSending) { return; } do { if (self.currentRecord >= RATE_LIMIT) { return; } if (self.sendQueueSize >= MAX_QUEUE_SIZE) { return; } if (self.processedFilesize >= self.totalFilesize) { return; } NSData *dataToWrite = [self.fileHandle readDataOfLength:BUFFER_SIZE]; self.currentRecord += dataToWrite.length; self.processedFilesize += dataToWrite.length; self.sendQueueSize += 1; [self.writeSocket writeData:dataToWrite withTimeout:_sendDataTimeout tag:0]; } while (1); } #pragma mark - #pragma mark Actions - (void)updateClearButton { [self.transferDialog updateClearButton]; } - (void)reloadStatusInformation { TDCFileTransferDialogTableCell *transferTableCell = self.transferTableCell; if (transferTableCell == nil) { return; } [transferTableCell reloadStatusInformation]; } #pragma mark - #pragma mark Properties - (nullable GCDAsyncSocket *)writeSocket { if (self.isReversed) { return self.connectionToRemoteServer; } else { return self.listeningServerConnectedClient; } } - (nullable GCDAsyncSocket *)readSocket { if (self.isReversed) { return self.listeningServerConnectedClient; } else { return self.connectionToRemoteServer; } } - (BOOL)isActingAsServer { return ((self.isSender && self.isReversed == NO) || (self.isSender == NO && self.isReversed)); } - (BOOL)isActingAsClient { return ((self.isSender == NO && self.isReversed == NO) || (self.isSender && self.isReversed)); } - (TDCFileTransferDialog *)transferDialog { return [TXSharedApplication sharedFileTransferDialog]; } - (NSArray *)speedRecords { @synchronized (self.speedRecordsPrivate) { return [self.speedRecordsPrivate copy]; } } - (nullable NSString *)filePath { NSString *path = self.path; NSString *filename = self.filename; if (path == nil || filename == nil) { return nil; } return [path stringByAppendingPathComponent:filename]; } - (nullable NSURL *)fileURL { NSString *filePath = self.filePath; if (filePath == nil) { return nil; } return [NSURL fileURLWithPath:filePath]; } - (uint64_t)currentFilesize { NSString *filePath = self.filePath; if (filePath == nil) { return 0; } if ([RZFileManager() fileExistsAtPath:filePath] == NO) { return 0; } NSDictionary *fileAttributes = [RZFileManager() attributesOfItemAtPath:filePath error:NULL]; return fileAttributes.fileSize; } - (void)setTransferStatus:(TDCFileTransferDialogTransferStatus)transferStatus { if (self->_transferStatus != transferStatus) { self->_transferStatus = transferStatus; [self reloadStatusInformation]; } } - (void)resetProperties { if (self.isResume == NO) { self.processedFilesize = 0; } self.currentRecord = 0; self.errorMessageDescription = nil; self.sendQueueSize = 0; @synchronized(self.speedRecordsPrivate) { [self.speedRecordsPrivate removeAllObjects]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/License Manager/Standalone/TDCLicenseManagerDialog.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSStringHelper.h" #import "NSViewHelper.h" #import "TXGlobalModels.h" #import "TXMasterController.h" #import "TXMenuController.h" #import "TPCPreferencesUserDefaults.h" #import "TDCLicenseManagerMigrateAppStoreSheetPrivate.h" #import "TDCLicenseManagerRecoverLostLicenseSheetPrivate.h" #import "TDCLicenseUpgradeActivateSheetPrivate.h" #import "TDCLicenseUpgradeCommonActionsPrivate.h" #import "TDCLicenseUpgradeDialogPrivate.h" #import "TDCLicenseUpgradeEligibilitySheetPrivate.h" #import "TDCProgressIndicatorSheetPrivate.h" #import "TLOTimer.h" #import "TLOLicenseManagerDownloaderPrivate.h" #import "TLOLicenseManagerLastGenPrivate.h" #import "TLOLicenseManagerPrivate.h" #import "TLOLocalization.h" #import "TLONotificationController.h" #import "TDCAlert.h" #import "TDCLicenseManagerDialogPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 NSString * const TDCLicenseManagerActivatedLicenseNotification = @"TDCLicenseManagerActivatedLicenseNotification"; NSString * const TDCLicenseManagerDeactivatedLicenseNotification = @"TDCLicenseManagerDeactivatedLicenseNotification"; NSString * const TDCLicenseManagerTrialExpiredNotification = @"TDCLicenseManagerTrialExpiredNotification"; #define _upgradeDialogRemindMeInterval 345600 // 4 days @interface TDCLicenseManagerDialog () @property (nonatomic, strong) IBOutlet NSView *contentViewUnregisteredTextualView; @property (nonatomic, strong) IBOutlet NSView *contentViewRegisteredTextualView; @property (nonatomic, weak) IBOutlet NSTextField *unregisteredViewLicenseKeyTextField; @property (nonatomic, weak) IBOutlet NSTextField *unregisteredViewTrialInformationTextField; @property (nonatomic, weak) IBOutlet NSTextField *registeredViewLicenseKeyTextField; @property (nonatomic, weak) IBOutlet NSTextField *registeredViewLicenseOwnerTextField; @property (nonatomic, weak) IBOutlet NSTextField *registeredViewLicensePurchaseDateTextField; @property (nonatomic, weak) IBOutlet NSButton *registeredViewDeactivateTextualButton; @property (nonatomic, weak) IBOutlet NSButton *unregisteredViewActivateTextualButton; @property (nonatomic, weak) IBOutlet NSButton *unregisteredViewCancelButton; @property (nonatomic, weak) IBOutlet NSButton *unregisteredViewRecoveryLostLicenseButton; @property (nonatomic, weak) IBOutlet NSImageView *unregisteredViewMacAppStoreIconImageView; @property (nonatomic, strong, nullable) TDCProgressIndicatorSheet *progressIndicator; @property (nonatomic, strong, nullable) TLOLicenseManagerDownloader *licenseManagerDownloader; @property (nonatomic, strong, nullable) TDCLicenseManagerMigrateAppStoreSheet *migrateAppStoreSheet; @property (nonatomic, strong, nullable) TDCLicenseManagerRecoverLostLicenseSheet *recoverLostLicenseSheet; @property (nonatomic, strong, nullable) TDCLicenseUpgradeDialog *upgradeDialog; @property (nonatomic, strong, nullable) TDCLicenseUpgradeActivateSheet *upgradeActivateSheet; @property (nonatomic, assign) BOOL textualIsRegistered; @property (nonatomic, assign) BOOL isSilentOnSuccess; @property (nonatomic, assign) BOOL operationInProgress; @property (nonatomic, strong) TLOTimer *trialTimer; - (IBAction)unregisteredViewActivateTextual:(id)sender; - (IBAction)unregisteredViewCancel:(id)sender; - (IBAction)unregisteredViewMigrateMacAppStorePurchase:(id)sender; - (IBAction)unregisteredViewPurchaseTextual:(id)sender; - (IBAction)unregisteredViewRecoveryLostLicense:(id)sender; - (IBAction)registeredViewDeactivateTextual:(id)sender; @end @interface TLONotificationController () - (void)scheduleLicenseManagerNotificationWithTitle:(NSString *)title message:(NSString *)message; @end @implementation TDCLicenseManagerDialog #pragma mark - #pragma mark Dialog Foundation - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return self; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCLicenseManagerDialog" owner:self topLevelObjects:nil]; [self populateMacAppStoreIconImageView]; [self updateSelectedPane]; } - (void)show { BOOL windowVisible = self.window.visible; [super show]; if (windowVisible == NO) { [self.window restoreWindowStateForClass:self.class]; } } - (void)applicationDidFinishLaunching { [self scheduleTimeRemainingInTrialNotification]; [self toggleTrialTimer]; [self activateLicenseKeyUsingArgumentDictionary]; [self upgradeDialogAppFinishedLaunchingRoutine]; } - (void)populateMacAppStoreIconImageView { /* Read the app store icon image from the actual App Store app so that we do not have to update it if Apple does. */ NSBundle *appStoreApplication = [NSBundle bundleWithPath:@"/Applications/App Store.app"]; if (appStoreApplication == nil) { appStoreApplication = [NSBundle bundleWithPath:@"/System/Applications/App Store.app"]; } if (appStoreApplication == nil) { return; } NSString *appStoreIconPath = [appStoreApplication pathForResource:@"AppIcon" ofType:@"icns"]; if (appStoreIconPath == nil) { return; } NSImage *appStoreIconImage = [[NSImage alloc] initWithContentsOfFile:appStoreIconPath]; self.unregisteredViewMacAppStoreIconImageView.image = appStoreIconImage; } - (void)updateSelectedPane { NSView *contentView = nil; self.textualIsRegistered = TLOLicenseManagerTextualIsRegistered(); if (self.textualIsRegistered) { NSString *licenseKey = TLOLicenseManagerLicenseKey(); NSString *licenseKeyOwner = TLOLicenseManagerLicenseOwnerName(); NSString *licenseKeyCreationDate = TLOLicenseManagerLicenseCreationDateFormatted(); self.registeredViewLicenseKeyTextField.stringValue = licenseKey; self.registeredViewLicenseOwnerTextField.stringValue = licenseKeyOwner; self.registeredViewLicensePurchaseDateTextField.stringValue = licenseKeyCreationDate; contentView = self.contentViewRegisteredTextualView; } else // textualIsRegistered { contentView = self.contentViewUnregisteredTextualView; NSString *formattedTrialInformation = [self timeRemainingInTrialFormattedMessage]; self.unregisteredViewTrialInformationTextField.stringValue = formattedTrialInformation; } [self.window replaceContentView:contentView]; } #pragma mark - #pragma mark Trial Timer - (void)toggleTrialTimer { if (TLOLicenseManagerTextualIsRegistered()) { [self stopTrialTimer]; } else { [self startTrialTimer]; } } - (void)startTrialTimer { if (self.trialTimer != nil) { return; } NSTimeInterval timeRemaining = (TLOLicenseManagerTimeRemainingTrial() * (-1.0)); if (timeRemaining == 0) { return; } TLOTimer *trialTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onTrialTimer]; }]; self.trialTimer = trialTimer; [trialTimer start:timeRemaining]; } - (void)stopTrialTimer { if (self.trialTimer == nil) { return; } [self.trialTimer stop]; self.trialTimer = nil; } - (void)onTrialTimer { [self stopTrialTimer]; [RZNotificationCenter() postNotificationName:TDCLicenseManagerTrialExpiredNotification object:self]; } #pragma mark - #pragma mark Activate License - (void)unregisteredViewPurchaseTextual:(id)sender { NSString *lastGenLicenseKey = [TLOLicenseManagerLastGen licenseKey]; if (lastGenLicenseKey != nil) { BOOL upgradeLicense = [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[dpo-ln]", lastGenLicenseKey.prettyLicenseKey) title:TXTLS(@"TLOLicenseManager[12h-3w]") defaultButton:TXTLS(@"TLOLicenseManager[qrx-aq]") alternateButton:TXTLS(@"TLOLicenseManager[zp8-8q]")]; if (upgradeLicense) { [self showUpgradeDialogForLicenseKey:lastGenLicenseKey]; return; } } [menuController() openStandaloneStoreWebpage:nil]; } - (void)unregisteredViewCancel:(id)sender { [self close]; } - (void)updateUnregisteredViewActivationButton { if (self.textualIsRegistered) { return; // Cancel operation... } NSString *licenseKeyValue = self.unregisteredViewLicenseKeyTextField.stringValue.trim; if (TLOLicenseManagerLicenseKeyIsValid(licenseKeyValue)) { self.unregisteredViewActivateTextualButton.enabled = YES; } else { self.unregisteredViewActivateTextualButton.enabled = NO; } } - (void)unregisteredViewActivateTextual:(id)sender { NSString *licenseKeyValue = self.unregisteredViewLicenseKeyTextField.stringValue; [self attemptToActivateLicenseKey:licenseKeyValue]; } - (void)activateLicenseKeyUsingArgumentDictionary { if (TLOLicenseManagerTextualIsRegistered()) { return; // Nothing to do here... } NSDictionary *commandLineArguments = [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSArgumentDomain]; NSString *argumentLicenseKey = commandLineArguments[@"-licenseKey"]; if (argumentLicenseKey == nil) { return; } [self activateLicenseKey:argumentLicenseKey silently:YES]; } - (void)activateLicenseKey:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); [self activateLicenseKey:licenseKey silently:NO]; } - (void)activateLicenseKey:(NSString *)licenseKey silently:(BOOL)silently { NSParameterAssert(licenseKey != nil); /* This method is allowed to be invoked by another class in order to activate a license. It is not invoked by this class on its own. */ if (TLOLicenseManagerLicenseKeyIsValid(licenseKey)) { [self attemptToActivateLicenseKey:licenseKey silently:silently]; } } - (void)attemptToActivateLicenseKey:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); [self attemptToActivateLicenseKey:licenseKey silently:NO]; } - (void)attemptToActivateLicenseKey:(NSString *)licenseKey silently:(BOOL)silently { NSParameterAssert(licenseKey != nil); /* User can click a link or do something else while an activation is already in progress, so we try to recognize when that happens so that we can prevent confusion when an operation is already in progress. */ /* This is the only place in the dialog where user can perform an action by outside influence. Everything else is internalized which means this check is only necessary here... at least for now. */ if (self.operationInProgress) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[vnu-5e]") title:TXTLS(@"TLOLicenseManager[0uc-io]", licenseKey.prettyLicenseKey) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; return; } /* Do not activate license we are already using. */ if (licenseKey == TLOLicenseManagerLicenseKey()) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[fxu-su]") title:TXTLS(@"TLOLicenseManager[bw3-sc]", licenseKey.prettyLicenseKey) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; return; } /* The activate sheet will close itself when user clicks to make purchase, but if user activates a license by other means (such as clicking the link in their e-mail), then we dismiss it here, so that we can do activation. */ [self closeUpgradeActivateSheet]; /* Perform activation */ self.isSilentOnSuccess = silently; [self beginProgressIndicator]; __weak TDCLicenseManagerDialog *weakSelf = self; TLOLicenseManagerDownloader *licenseManagerDownloader = [TLOLicenseManagerDownloader new]; licenseManagerDownloader.actionBlock = ^BOOL(NSUInteger statusCode, id _Nullable statusContext) { return (TLOLicenseManagerWriteLicenseFileContents(statusContext) == TLOLicenseManagerActionResultSuccess); }; licenseManagerDownloader.errorBlock = ^BOOL(NSUInteger statusCode, id _Nullable statusContext) { if (statusCode != 0 || statusContext == nil) { return NO; } /* lastGenLicenseKey will only be non-nil if the contents of licenseContents are for a license that have not been upgraded to Textual 7 yet. */ NSString *lastGenLicenseKey = [TLOLicenseManagerLastGen licenseKeyForLicenseContents:statusContext]; if (lastGenLicenseKey != nil) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[5bu-ov]") title:TXTLS(@"TLOLicenseManager[pmv-qp]", lastGenLicenseKey.prettyLicenseKey) defaultButton:TXTLS(@"TLOLicenseManager[53u-lt]") alternateButton:nil]; [weakSelf showUpgradeDialogForLicenseKey:lastGenLicenseKey]; /* We return YES so no error is displayed except the one above. */ return YES; } /* We did not handle the error. */ return NO; }; licenseManagerDownloader.completionBlock = ^(BOOL operationSuccessful, NSUInteger statusCode, id _Nullable statusContext) { [weakSelf licenseManagerDownloaderCompletionBlock]; if (operationSuccessful) { weakSelf.unregisteredViewLicenseKeyTextField.stringValue = @""; [weakSelf toggleTrialTimer]; [RZNotificationCenter() postNotificationName:TDCLicenseManagerActivatedLicenseNotification object:weakSelf]; /* We close the upgrade dialog if a key is activated because why would we keep it around under that condition? */ [weakSelf closeUpgradeDialog]; /* Reset context for activate sheet. */ [weakSelf resetUpgradeActivateSheetContext]; } weakSelf.operationInProgress = NO; }; self.operationInProgress = YES; licenseManagerDownloader.isSilentOnSuccess = self.isSilentOnSuccess; [licenseManagerDownloader activateLicense:licenseKey.trim]; self.licenseManagerDownloader = licenseManagerDownloader; } #pragma mark - #pragma mark License Upgrade Launch Routine - (void)upgradeDialogAppFinishedLaunchingRoutine { /* There is no reason to perform this check if this copy of Textual is already registered. */ if (TLOLicenseManagerTextualIsRegistered()) { /* If Textual is registered, then we reset these values because there is no reason to have them hanging around. */ [self resetUpgradeActivateSheetContext]; return; } /* Check if we have license key saved from Textual 6 upgrade dialog. */ if ([self showUpgradeActivateSheet]) { return; } /* Show upgrade dialog */ [self showUpgradeDialogForLastGenLicense]; } - (void)showUpgradeDialogForLastGenLicense { /* Is there even a last generation key? */ NSString *lastGenLicenseKey = [TLOLicenseManagerLastGen licenseKey]; if (lastGenLicenseKey == nil) { return; } /* When was the last time the user was nagged? */ /* Unless user hits "Remind Me Later", then the upgrade dialog is not hidden due to time limit. This change was made so that if the user is performing some type of action that causes them to miss the dialog, such as installing an update, it will show next launch. */ BOOL remindMeLater = [RZUserDefaults() boolForKey:@"Textual 7 Upgrade -> Tv7 -> Remind Me Later"]; NSTimeInterval lastCheckTime = [RZUserDefaults() doubleForKey:@"Textual 7 Upgrade -> Tv7 -> Last Dialog Presentation (LMD)"]; NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; if (lastCheckTime > 0 && remindMeLater) { if ((currentTime - lastCheckTime) < _upgradeDialogRemindMeInterval) { LogToConsoleInfo("Not enough time has passed since last presentation"); return; } } [RZUserDefaults() setDouble:currentTime forKey:@"Textual 7 Upgrade -> Tv7 -> Last Dialog Presentation (LMD)"]; [RZUserDefaults() removeObjectForKey:@"Textual 7 Upgrade -> Tv7 -> Remind Me Later"]; /* Nag user */ [self showUpgradeDialogForLicenseKey:lastGenLicenseKey]; } #pragma mark - #pragma mark License Upgrade Dialog - (void)showUpgradeDialogForLicenseKey:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); if (self.upgradeDialog) { if ([self.upgradeDialog.licenseKey isEqualToString:licenseKey]) { [self.upgradeDialog show]; return; } else { [self.upgradeDialog close]; } } TDCLicenseUpgradeDialog *upgradeDialog = [[TDCLicenseUpgradeDialog alloc] initWithLicenseKey:licenseKey]; upgradeDialog.delegate = self; [upgradeDialog show]; self.upgradeDialog = upgradeDialog; } - (void)closeUpgradeDialog { if (self.upgradeDialog) { [self.upgradeDialog close]; } } - (void)licenseUpgradeDialogEligibilityChanged:(TDCLicenseUpgradeDialog *)sender { /* Record eligibility status so that we might present an activate sheet next time the user opens the app. */ [self setUpgradeActivateSheetContextWithDialog:sender]; } - (void)licenseUpgradeDialogWRemindMeLater:(TDCLicenseUpgradeDialog *)sender { [RZUserDefaults() setBool:YES forKey:@"Textual 7 Upgrade -> Tv7 -> Remind Me Later"]; } - (void)licenseUpgradeDialogWillClose:(TDCLicenseUpgradeDialog *)sender { self.upgradeDialog = nil; } #pragma mark - #pragma mark Upgrade Activate Sheet - (BOOL)showUpgradeActivateSheet { /* Do we have a properly formatted license key? */ NSString *licenseKey = [RZUserDefaults() objectForKey:@"Textual 7 Upgrade -> Tv7 -> Eligible License Key"]; if (licenseKey == nil) { return NO; } if (TLOLicenseManagerLicenseKeyIsValid(licenseKey) == NO) { return NO; } /* Do we have an eligibility that is acceptable? */ NSUInteger eligibility = [RZUserDefaults() unsignedIntegerForKey:@"Textual 7 Upgrade -> Tv7 -> Eligibility"]; if (eligibility != TLOLicenseUpgradeEligibilityDiscount && eligibility != TLOLicenseUpgradeEligibilityFree && eligibility != TLOLicenseUpgradeEligibilityAlreadyUpgraded) { return NO; } /* Show ourselves because we will place the sheet on self. */ [self show]; /* Create sheet and start it. */ TDCLicenseUpgradeActivateSheet *activateSheet = [[TDCLicenseUpgradeActivateSheet alloc] initWithLicenseKey:licenseKey eligibility:eligibility]; activateSheet.delegate = self; activateSheet.window = self.window; [activateSheet start]; self.upgradeActivateSheet = activateSheet; /* Inform launch routine to do nothing more. */ return YES; } - (void)closeUpgradeActivateSheet { if (self.upgradeActivateSheet) { [self.upgradeActivateSheet endSheet]; } } - (void)setUpgradeActivateSheetContextWithDialog:(TDCLicenseUpgradeDialog *)upgradeDialog { NSParameterAssert(upgradeDialog != nil); [RZUserDefaults() setUnsignedInteger:upgradeDialog.eligibility forKey:@"Textual 7 Upgrade -> Tv7 -> Eligibility"]; [RZUserDefaults() setObject:upgradeDialog.licenseKey forKey:@"Textual 7 Upgrade -> Tv7 -> Eligible License Key"]; } - (void)resetUpgradeActivateSheetContext { /* We reset these values for any successful activation. It is probably more proper to compare the key activated to the context, but I don't think it will matter in the real world. */ [RZUserDefaults() removeObjectForKey:@"Textual 7 Upgrade -> Tv7 -> Eligibility"]; [RZUserDefaults() removeObjectForKey:@"Textual 7 Upgrade -> Tv7 -> Eligible License Key"]; } - (void)upgradeActivateSheetActivateLicense:(TDCLicenseUpgradeActivateSheet *)sender { NSString *licenseKey = sender.licenseKey; [sender endSheet]; [self attemptToActivateLicenseKey:licenseKey]; } - (void)upgradeActivateSheetPurchaseUpgrade:(TDCLicenseUpgradeActivateSheet *)sender { [TDCLicenseUpgradeCommonActions purchaseUpgradeForLicense:sender.licenseKey]; } - (void)upgradeActivateSheetSuppressed:(TDCLicenseUpgradeActivateSheet *)sender { [self resetUpgradeActivateSheetContext]; } - (void)upgradeActivateSheetWillClose:(TDCLicenseUpgradeActivateSheet *)sender { self.upgradeActivateSheet = nil; } #pragma mark - #pragma mark Recover Lost License - (void)unregisteredViewRecoveryLostLicense:(id)sender { TDCLicenseManagerRecoverLostLicenseSheet *recoverLostLicenseSheet = [[TDCLicenseManagerRecoverLostLicenseSheet alloc] initWithWindow:self.window]; recoverLostLicenseSheet.delegate = self; [recoverLostLicenseSheet start]; self.recoverLostLicenseSheet = recoverLostLicenseSheet; } - (void)licenseManagerRecoverLostLicenseSheet:(TDCLicenseManagerRecoverLostLicenseSheet *)sender onOk:(NSString *)contactAddress { __weak TDCLicenseManagerDialog *weakSelf = self; TLOLicenseManagerDownloader *licenseManagerDownloader = [TLOLicenseManagerDownloader new]; licenseManagerDownloader.completionBlock = ^(BOOL operationSuccessful, NSUInteger statusCode, id _Nullable statusContext) { [weakSelf licenseManagerDownloaderCompletionBlock]; }; licenseManagerDownloader.isSilentOnSuccess = self.isSilentOnSuccess; [licenseManagerDownloader requestLostLicenseKeyForContactAddress:contactAddress]; self.licenseManagerDownloader = licenseManagerDownloader; } - (void)licenseManagerRecoverLostLicenseSheetWillClose:(TDCLicenseManagerRecoverLostLicenseSheet *)sender { self.recoverLostLicenseSheet = nil; if (self.licenseManagerDownloader) { [self beginProgressIndicator]; } } #pragma mark - #pragma mark Deactivate License - (void)registeredViewDeactivateTextual:(id)sender { BOOL deactivateCopy = [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[z87-wb]") title:TXTLS(@"TLOLicenseManager[pg1-a9]") defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]")]; if (deactivateCopy == NO) { return; // Cancel operation... } /* License deactivation does not use a progress indicator because it does not have to touch the network. It will only delete a file on the hard drive which is typically instant. */ __weak TDCLicenseManagerDialog *weakSelf = self; TLOLicenseManagerDownloader *licenseManagerDownloader = [TLOLicenseManagerDownloader new]; licenseManagerDownloader.actionBlock = ^BOOL(NSUInteger statusCode, id _Nullable statusContext) { return (TLOLicenseManagerDeleteLicenseFile() == TLOLicenseManagerActionResultSuccess); }; licenseManagerDownloader.completionBlock = ^(BOOL operationSuccessful, NSUInteger statusCode, id _Nullable statusContext) { [weakSelf licenseManagerDownloaderCompletionBlock]; [weakSelf toggleTrialTimer]; [RZNotificationCenter() postNotificationName:TDCLicenseManagerDeactivatedLicenseNotification object:weakSelf]; }; licenseManagerDownloader.isSilentOnSuccess = self.isSilentOnSuccess; [licenseManagerDownloader deactivateLicense]; // self.licenseManagerDownloader = licenseManagerDownloader; } #pragma mark - #pragma mark Mac App Store Receipt Processing - (void)unregisteredViewMigrateMacAppStorePurchase:(id)sender { TDCLicenseManagerMigrateAppStoreSheet *migrateAppStoreSheet = [[TDCLicenseManagerMigrateAppStoreSheet alloc] initWithWindow:self.window]; migrateAppStoreSheet.delegate = self; [migrateAppStoreSheet start]; self.migrateAppStoreSheet = migrateAppStoreSheet; } - (void)licenseManagerMigrateAppStoreSheet:(TDCLicenseManagerMigrateAppStoreSheet *)sender convertReceipt:(NSString *)receiptData licenseOwnerName:(NSString *)licenseOwnerName licenseOwnerContactAddress:(NSString *)licenseOwnerContactAddress { __weak TDCLicenseManagerDialog *weakSelf = self; TLOLicenseManagerDownloader *licenseManagerDownloader = [TLOLicenseManagerDownloader new]; licenseManagerDownloader.completionBlock = ^(BOOL operationSuccessful, NSUInteger statusCode, id _Nullable statusContext) { [weakSelf licenseManagerDownloaderCompletionBlock]; }; licenseManagerDownloader.isSilentOnSuccess = self.isSilentOnSuccess; [licenseManagerDownloader migrateMacAppStorePurchase:receiptData licenseOwnerName:licenseOwnerName licenseOwnerContactAddress:licenseOwnerContactAddress]; self.licenseManagerDownloader = licenseManagerDownloader; } - (void)licenseManagerMigrateAppStoreSheetWillClose:(TDCLicenseManagerMigrateAppStoreSheet *)sender { self.migrateAppStoreSheet = nil; if (self.licenseManagerDownloader) { [self beginProgressIndicator]; } } #pragma mark - #pragma mark NSTextField Delegate - (void)controlTextDidChange:(NSNotification *)obj { if (obj.object == self.unregisteredViewLicenseKeyTextField) { [self updateUnregisteredViewActivationButton]; } } #pragma mark - #pragma mark Helper Methods - (void)licenseManagerDownloaderCompletionBlock { self.licenseManagerDownloader = nil; [self endProgressIndicator]; [self updateSelectedPane]; [self updateUnregisteredViewActivationButton]; self.isSilentOnSuccess = NO; // Reset flag regardless of state. // This flag is one-time use. } - (void)beginProgressIndicator { self.progressIndicator = [[TDCProgressIndicatorSheet alloc] initWithWindow:self.window]; [self.progressIndicator start]; } - (void)endProgressIndicator { if (self.progressIndicator == nil) { return; } [self.progressIndicator stop]; self.progressIndicator = nil; } #pragma mark - #pragma mark Notification Center - (NSString *)timeRemainingInTrialFormattedMessage { NSTimeInterval timeLeft = TLOLicenseManagerTimeRemainingTrial(); if (timeLeft >= 0) { return TXTLS(@"TLOLicenseManager[kn7-ju]"); } NSString *formattedTimeRemainingString = TXHumanReadableTimeInterval(timeLeft, YES, NSCalendarUnitDay); return TXTLS(@"TLOLicenseManager[wdl-3f]", formattedTimeRemainingString); } - (void)scheduleTimeRemainingInTrialNotification { if (TLOLicenseManagerTextualIsRegistered() || TLOLicenseManagerIsTrialExpired()) { return; // Do not schedule notification... } NSString *title = [self timeRemainingInTrialFormattedMessage]; NSString *message = TXTLS(@"TLOLicenseManager[ccj-ag]"); [sharedNotificationController() scheduleLicenseManagerNotificationWithTitle:title message:message]; } #pragma mark - #pragma mark Window Delegate - (void)windowWillClose:(NSNotification *)note { [self.window saveWindowStateForClass:self.class]; } @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/License Manager/Standalone/TDCLicenseManagerMigrateAppStoreSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "TPCPathInfo.h" #import "TLOLocalization.h" #import "TVCValidatedTextField.h" #import "TDCLicenseManagerMigrateAppStoreSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN /* * TDCLicenseManagerMigrateAppStoreSheet presents an open dialog to * find a copy of Textual with a receipt then presents a sheet asking * for the user's e-mail address as long as a valid receipt is present. */ #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 #define _licenseOwnerNameMaximumLength 255 #define _licenseOwnerContactAddressMaximumLength 2000 @interface TDCLicenseManagerMigrateAppStoreSheet () @property (nonatomic, weak) IBOutlet TVCValidatedTextField *licenseOwnerNameTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *licenseOwnerContactAddressTextField; @property (nonatomic, copy) NSString *cachedReceiptData; @end @implementation TDCLicenseManagerMigrateAppStoreSheet #pragma mark - #pragma mark Contact Address Sheet - (instancetype)initWithWindow:(nullable NSWindow *)window { if ((self = [super initWithWindow:window])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCLicenseManagerMigrateAppStoreSheet" owner:self topLevelObjects:nil]; } - (void)startContactAddressSheet { /* E-mail address text field configuration */ self.licenseOwnerContactAddressTextField.stringValueIsInvalidOnEmpty = YES; self.licenseOwnerContactAddressTextField.stringValueUsesOnlyFirstToken = YES; self.licenseOwnerContactAddressTextField.textDidChangeCallback = self; self.licenseOwnerContactAddressTextField.stringValue = [XRAddressBook myEmailAddress]; self.licenseOwnerContactAddressTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([currentValue containsCharactersFromCharacterSet:[NSCharacterSet newlineCharacterSet]]) { return TXTLS(@"CommonErrors[gas-v8]"); } if (currentValue.length > _licenseOwnerContactAddressMaximumLength) { return TXTLS(@"CommonErrors[2cb-af]", _licenseOwnerContactAddressMaximumLength); } return nil; }; /* First & last name text field configuration */ self.licenseOwnerNameTextField.stringValueIsInvalidOnEmpty = YES; self.licenseOwnerNameTextField.stringValueUsesOnlyFirstToken = NO; self.licenseOwnerNameTextField.textDidChangeCallback = self; self.licenseOwnerNameTextField.stringValue = [XRAddressBook myName]; self.licenseOwnerNameTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([currentValue containsCharactersFromCharacterSet:[NSCharacterSet newlineCharacterSet]]) { return TXTLS(@"CommonErrors[gas-v8]"); } if (currentValue.length > _licenseOwnerNameMaximumLength) { return TXTLS(@"CommonErrors[2cb-af]", _licenseOwnerNameMaximumLength); } return nil; }; /* Begin sheet... */ [self startSheet]; } - (void)start { [self findTextualUsingLaunchServices]; } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } NSString *receiptData = self.cachedReceiptData; NSString *licenseOwnerName = self.licenseOwnerNameTextField.value; NSString *licenseOwnerContactAddress = self.licenseOwnerContactAddressTextField.value; [self.delegate licenseManagerMigrateAppStoreSheet:self convertReceipt:receiptData licenseOwnerName:licenseOwnerName licenseOwnerContactAddress:licenseOwnerContactAddress]; [super ok:sender]; } - (BOOL)okOrError { if ([self okOrErrorForTextField:self.licenseOwnerNameTextField] == NO) { return NO; } if ([self okOrErrorForTextField:self.licenseOwnerContactAddressTextField] == NO) { return NO; } return YES; } #pragma mark - #pragma mark Open Dialog - (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError **)outError { NSString *applicationName = nil; if ([self canUseApplicationAtURL:url applicationName:&applicationName] == NO) { if (outError) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[NSURLErrorKey] = url; userInfo[NSLocalizedDescriptionKey] = TXTLS(@"TLOLicenseManager[b6l-ka]", applicationName); userInfo[NSLocalizedRecoverySuggestionErrorKey] = TXTLS(@"TLOLicenseManager[sdj-xd]"); *outError = [NSError errorWithDomain:TXErrorDomain code:27984 userInfo:userInfo]; } return NO; } return YES; } - (void)findTextualUsingLaunchServices { CFURLRef searchScopeURL = (__bridge CFURLRef)[NSURL URLWithString:@"textual://knowledge-base"]; NSArray *matchedApplications = (__bridge_transfer NSArray *)LSCopyApplicationURLsForURL(searchScopeURL, kLSRolesViewer); NSURL *matchedApplication = nil; for (NSURL *applicationURL in matchedApplications) { if ([self canUseApplicationAtURL:applicationURL applicationName:NULL]) { matchedApplication = applicationURL; LogToConsoleInfo("Automatically detected Mac App Store Textual 7 at the following path: %{public}@", matchedApplication.standardizedTildePath); break; } } if (matchedApplication) { [self haveApplicationAtURL:matchedApplication]; } else { [self presentOpenDialog]; } } - (void)presentOpenDialog { NSOpenPanel *d = [NSOpenPanel openPanel]; NSURL *applicationsPath = [TPCPathInfo systemApplicationsURL]; if (applicationsPath) { d.directoryURL = applicationsPath; } d.allowedFileTypes = @[@"app"]; d.delegate = (id)self; d.allowsMultipleSelection = NO; d.canChooseDirectories = NO; d.canChooseFiles = YES; d.canCreateDirectories = NO; d.resolvesAliases = YES; d.message = TXTLS(@"TLOLicenseManager[vft-qo]"); d.prompt = TXTLS(@"Prompts[xne-79]"); [d beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [self haveApplicationAtURL:d.URL]; } else { [self cancel:nil]; } }]; } - (void)haveApplicationAtURL:(NSURL *)applicationURL { NSParameterAssert(applicationURL != nil); NSString *receiptData = [self receiptDataForApplicationAtURL:applicationURL]; if (receiptData) { self.cachedReceiptData = receiptData; XRPerformBlockAsynchronouslyOnMainQueue(^{ [self startContactAddressSheet]; }); return; } [self cancel:nil]; } #pragma mark - #pragma mark Helper Methods - (nullable NSString *)receiptDataForApplicationAtURL:(NSURL *)applicationURL { NSParameterAssert(applicationURL != nil); /* Locate application bundle and determine whether the receipt file is even contained within it. */ NSBundle *applicationBundle = [NSBundle bundleWithURL:applicationURL]; if (applicationBundle == nil) { return nil; } NSURL *receiptFileURL = applicationBundle.appStoreReceiptURL; if (receiptFileURL == nil) { return nil; } if ([RZFileManager() fileExistsAtURL:receiptFileURL] == NO) { return nil; } /* If the receipt file exists, request the data and cache the base64 value of the receipt data. */ NSError *receiptDataReadError = nil; NSData *receiptData = [NSData dataWithContentsOfURL:receiptFileURL options:0 error:&receiptDataReadError]; if (receiptData == nil) { LogToConsoleError("Failed to read the contents of the receipt file '%{public}@': %{public}@", receiptFileURL.standardizedTildePath, receiptDataReadError.localizedDescription); return nil; } return [XRBase64Encoding encodeData:receiptData]; } - (BOOL)canUseApplicationAtURL:(NSURL *)applicationURL applicationName:(NSString * _Nullable * _Nullable)applicationName { /* The license API performs validation of the uploaded receipt which means that the the only thing we need to know at this point is that the bundle identifier of the application is a known value and that the receipt file actually does exist in the application. */ NSBundle *applicationBundle = [NSBundle bundleWithURL:applicationURL]; if (applicationBundle == nil) { return NO; } /* Pass to caller the application name of the bundle. */ if ( applicationName) { *applicationName = applicationBundle.displayName; } /* Compare bundle identifier. */ /* The bundle identifier used for comparison is hard coded is because the bundle identifier for the running copy can be different than the value that we are expecting from an older copy which we cannot change. */ NSString *applicationBundleID = [applicationBundle objectForInfoDictionaryKey:@"CFBundleIdentifier"]; if (applicationBundleID == nil || ([applicationBundleID isEqualToString:@"com.codeux.irc.textual5"] == NO && [applicationBundleID isEqualToString:@"com.codeux.apps.textual-mas"] == NO)) { return NO; } /* Determine whether a receipt file exists. */ NSURL *receiptFileURL = applicationBundle.appStoreReceiptURL; if (receiptFileURL == nil) { return NO; } if ([RZFileManager() fileExistsAtURL:receiptFileURL] == NO) { return NO; } /* Return successful result */ return YES; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self.delegate licenseManagerMigrateAppStoreSheetWillClose:self]; } @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/License Manager/Standalone/TDCLicenseManagerRecoverLostLicenseSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOLocalization.h" #import "TVCValidatedTextField.h" #import "TDCLicenseManagerRecoverLostLicenseSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 #define _licenseOwnerContactAddressMaximumLength 2000 @interface TDCLicenseManagerRecoverLostLicenseSheet () @property (nonatomic, weak) IBOutlet TVCValidatedTextField *contactAddressTextField; @end @implementation TDCLicenseManagerRecoverLostLicenseSheet - (instancetype)initWithWindow:(nullable NSWindow *)window { if ((self = [super initWithWindow:window])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCLicenseManagerRecoverLostLicenseSheet" owner:self topLevelObjects:nil]; } - (void)start { self.contactAddressTextField.stringValueIsInvalidOnEmpty = YES; self.contactAddressTextField.stringValueUsesOnlyFirstToken = YES; self.contactAddressTextField.textDidChangeCallback = self; self.contactAddressTextField.stringValue = [XRAddressBook myEmailAddress]; self.contactAddressTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([currentValue containsCharactersFromCharacterSet:[NSCharacterSet newlineCharacterSet]]) { return TXTLS(@"CommonErrors[gas-v8]"); } if (currentValue.length > _licenseOwnerContactAddressMaximumLength) { return TXTLS(@"CommonErrors[2cb-af]", _licenseOwnerContactAddressMaximumLength); } return nil; }; [self startSheet]; } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } NSString *contactAddress = self.contactAddressTextField.value; [self.delegate licenseManagerRecoverLostLicenseSheet:self onOk:contactAddress]; [super ok:sender]; } - (BOOL)okOrError { return [self okOrErrorForTextField:self.contactAddressTextField]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self.delegate licenseManagerRecoverLostLicenseSheetWillClose:self]; } @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/License Manager/Standalone/TDCLicenseUpgradeActivateSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "TDCLicenseUpgradeActivateSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @interface TDCLicenseUpgradeActivateSheet () @property (nonatomic, copy, readwrite) NSString *licenseKey; @property (nonatomic, assign, readwrite) TLOLicenseUpgradeEligibility eligibility; @property (nonatomic, strong) IBOutlet NSWindow *sheetEligibleDiscount; @property (nonatomic, strong) IBOutlet NSWindow *sheetEligibleFree; @property (nonatomic, weak) IBOutlet NSTextField *sheetEligibleDiscountTitleTextField; @property (nonatomic, weak) IBOutlet NSTextField *sheetEligibleFreeTitleTextField; @property (nonatomic, weak) IBOutlet NSButton *sheetEligibleDiscountSuppressionButton; @property (nonatomic, weak) IBOutlet NSButton *sheetEligibleFreeSuppressionButton; - (IBAction)actionActivateLicense:(id)sender; - (IBAction)actionPurchaseUpgrade:(id)sender; - (IBAction)actionCancel:(id)sender; @end @implementation TDCLicenseUpgradeActivateSheet #pragma mark - #pragma mark Dialog Foundation - (instancetype)initWithLicenseKey:(NSString *)licenseKey eligibility:(TLOLicenseUpgradeEligibility)eligibility { NSParameterAssert(licenseKey != nil); if ((self = [super initWithWindow:nil])) { self.licenseKey = licenseKey; self.eligibility = eligibility; [self prepareInitialState]; return self; } return self; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCLicenseUpgradeActivateSheet" owner:self topLevelObjects:nil]; } - (void)start { [self startSheet]; } - (void)startSheet { NSTextField *sheetTitleTextField = nil; if (self.eligibility == TLOLicenseUpgradeEligibilityDiscount) { self.sheet = self.sheetEligibleDiscount; sheetTitleTextField = self.sheetEligibleDiscountTitleTextField; } else if (self.eligibility == TLOLicenseUpgradeEligibilityFree || self.eligibility == TLOLicenseUpgradeEligibilityAlreadyUpgraded) { self.sheet = self.sheetEligibleFree; sheetTitleTextField = self.sheetEligibleFreeTitleTextField; } else { NSAssert(NO, @"Cannot display a sheet for this type of eligibility"); } sheetTitleTextField.stringValue = [NSString stringWithFormat: sheetTitleTextField.stringValue, self.licenseKey.prettyLicenseKey]; [super startSheet]; } - (void)actionActivateLicense:(id)sender { [self.delegate upgradeActivateSheetActivateLicense:self]; } - (void)actionPurchaseUpgrade:(id)sender { [self.delegate upgradeActivateSheetPurchaseUpgrade:self]; } - (void)actionCancel:(id)sender { /* Only one of two sheets can ever be visible so just check if one is on. */ if (self.sheetEligibleDiscountSuppressionButton.state == NSControlStateValueOn || self.sheetEligibleFreeSuppressionButton.state == NSControlStateValueOn) { [self.delegate upgradeActivateSheetSuppressed:self]; } [self endSheet]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self.delegate upgradeActivateSheetWillClose:self]; } @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/License Manager/Standalone/TDCLicenseUpgradeCommonActions.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" #import "TXMenuControllerPrivate.h" #import "TLOpenLink.h" #import "TDCLicenseUpgradeCommonActionsPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @implementation TDCLicenseUpgradeCommonActions + (void)contactSupport { [menuController() contactSupport:nil]; } + (void)activateLicense:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); [menuController() manageLicense:nil activateLicenseKey:licenseKey]; } + (void)purchaseUpgradeForLicense:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); NSString *linkToOpen = [NSString stringWithFormat:@"https://www.codeux.com/textual/version-7-upgrade/upgradeLicense/%@", licenseKey]; [TLOpenLink openWithString:linkToOpen inBackground:NO]; } + (void)learnMore { NSURL *urlToOpen = [NSURL URLWithString:@"https://www.codeux.com/textual/version-7-upgrade/learnMore"]; [TLOpenLink open:urlToOpen inBackground:NO]; } + (void)openStandaloneStore { [menuController() openStandaloneStoreWebpage:nil]; } @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/License Manager/Standalone/TDCLicenseUpgradeDialog.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TLOLicenseManagerPrivate.h" #import "TDCLicenseManagerDialogPrivate.h" #import "TDCLicenseUpgradeCommonActionsPrivate.h" #import "TDCLicenseUpgradeDialogPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @interface TDCLicenseUpgradeDialog () @property (nonatomic, copy, readwrite) NSString *licenseKey; @property (nonatomic, assign, readwrite) TLOLicenseUpgradeEligibility eligibility; @property (nonatomic, strong, nullable) TDCLicenseUpgradeEligibilitySheet *EligibilitySheet; @property (nonatomic, weak) IBOutlet NSButton *upgradeDialogContinueTrialButton; @property (nonatomic, weak) IBOutlet NSTextField *upgradeDialogTrialPeriodTextField; - (IBAction)actionLearnMore:(id)sender; - (IBAction)actionPurchaseUpgrade:(id)sender; - (IBAction)actionContinueTrial:(id)sender; - (IBAction)actionRemindMeLater:(id)sender; @end @implementation TDCLicenseUpgradeDialog #pragma mark - #pragma mark Dialog Foundation - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithLicenseKey:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); if ((self = [super init])) { self.licenseKey = licenseKey; [self prepareInitialState]; return self; } return self; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCLicenseUpgradeDialog" owner:self topLevelObjects:nil]; self.eligibility = TLOLicenseUpgradeEligibilityUnknown; } - (void)show { self.upgradeDialogContinueTrialButton.enabled = (TLOLicenseManagerIsTrialExpired() == NO); self.upgradeDialogTrialPeriodTextField.stringValue = [[TXSharedApplication sharedLicenseManagerDialog] timeRemainingInTrialFormattedMessage]; [super show]; } #pragma mark - #pragma mark Eligibility Sheet Actions - (void)checkEligibility { if (self.EligibilitySheet != nil) { return; } TDCLicenseUpgradeEligibilitySheet *eligibilitySheet = [[TDCLicenseUpgradeEligibilitySheet alloc] initWithLicenseKey:self.licenseKey]; eligibilitySheet.delegate = self; eligibilitySheet.window = self.window; [eligibilitySheet checkEligibility]; self.EligibilitySheet = eligibilitySheet; } - (void)closeEligibilitySheet { if (self.EligibilitySheet) { [self.EligibilitySheet endSheet]; } } - (void)upgradeEligibilitySheetContactSupport:(TDCLicenseUpgradeEligibilitySheet *)sender { [TDCLicenseUpgradeCommonActions contactSupport]; } - (void)upgradeEligibilitySheetActivateLicense:(TDCLicenseUpgradeEligibilitySheet *)sender { [TDCLicenseUpgradeCommonActions activateLicense:sender.licenseKey]; } - (void)upgradeEligibilitySheetPurchaseUpgrade:(TDCLicenseUpgradeEligibilitySheet *)sender { [TDCLicenseUpgradeCommonActions purchaseUpgradeForLicense:sender.licenseKey]; } - (void)upgradeEligibilitySheetPurchaseStandalone:(TDCLicenseUpgradeEligibilitySheet *)sender { [TDCLicenseUpgradeCommonActions openStandaloneStore]; } - (void)upgradeEligibilitySheetChanged:(TDCLicenseUpgradeEligibilitySheet *)sender { self.eligibility = sender.eligibility; [self.delegate licenseUpgradeDialogEligibilityChanged:self]; } - (void)upgradeEligibilitySheetWillClose:(TDCLicenseUpgradeEligibilitySheet *)sender { self.EligibilitySheet = nil; } #pragma mark - #pragma mark Upgrade Dialog Actions - (void)actionLearnMore:(id)sender { [TDCLicenseUpgradeCommonActions learnMore]; } - (void)actionPurchaseUpgrade:(id)sender { [self checkEligibility]; } - (void)actionContinueTrial:(id)sender { [self.delegate licenseUpgradeDialogWRemindMeLater:self]; [self close]; } - (void)actionRemindMeLater:(id)sender { [self.delegate licenseUpgradeDialogWRemindMeLater:self]; [self close]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self closeEligibilitySheet]; [self.delegate licenseUpgradeDialogWillClose:self]; } @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/License Manager/Standalone/TDCLicenseUpgradeEligibilitySheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "TXGlobalModels.h" #import "TLOLocalization.h" #import "TLOLicenseManagerPrivate.h" #import "TLOLicenseManagerDownloaderPrivate.h" #import "TDCAlert.h" #import "TDCProgressIndicatorSheetPrivate.h" #import "TDCLicenseUpgradeEligibilitySheetPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @interface TDCLicenseUpgradeEligibilitySheet () @property (nonatomic, copy, readwrite) NSString *licenseKey; @property (nonatomic, assign, readwrite) TLOLicenseUpgradeEligibility eligibility; @property (nonatomic, strong) TLOLicenseManagerDownloader *licenseManagerDownloader; @property (nonatomic, strong) TDCProgressIndicatorSheet *progressIndicator; @property (nonatomic, strong) IBOutlet NSWindow *sheetNotEligible; @property (nonatomic, strong) IBOutlet NSWindow *sheetEligibleDiscount; @property (nonatomic, strong) IBOutlet NSWindow *sheetEligibleFree; @property (nonatomic, assign) BOOL checkingEligibility; - (IBAction)actionContactSupport:(id)sender; - (IBAction)actionActivateLicense:(id)sender; - (IBAction)actionPurchaseUpgrade:(id)sender; - (IBAction)actionPurchaseStandalone:(id)sender; - (IBAction)actionClose:(id)sender; @end @implementation TDCLicenseUpgradeEligibilitySheet #pragma mark - #pragma mark Dialog Foundation - (instancetype)initWithLicenseKey:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); if ((self = [super initWithWindow:nil])) { self.licenseKey = licenseKey; [self prepareInitialState]; return self; } return self; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCLicenseUpgradeEligibilitySheet" owner:self topLevelObjects:nil]; self.eligibility = TLOLicenseUpgradeEligibilityUnknown; } - (void)startSheet { LogToConsoleError("This method does nothing. Use -checkEligibility instead"); LogStackTrace(); } - (void)endSheet { [self _cancelEligibilityCheck]; [super endSheet]; } - (void)endSheetEarly { /* If we end sheet early, before self.sheet is ever defined, then trying to close it wont fire this delegate call. We fake the call to the delegate so that the sheet can be released. */ [self windowWillClose:nil]; } - (void)checkEligibility { [self _checkEligibility]; } #pragma mark - #pragma mark Utilities - (void)_beginProgressIndicator { [self _beginProgressIndicatorInWindow:self.window]; } - (void)_beginProgressIndicatorInWindow:(NSWindow *)window { self.progressIndicator = [[TDCProgressIndicatorSheet alloc] initWithWindow:window]; [self.progressIndicator start]; } - (void)_endProgressIndicator { if (self.progressIndicator == nil) { return; } [self.progressIndicator stop]; self.progressIndicator = nil; } #pragma mark - #pragma mark Eligibility Sheet Actions - (void)_presentEligibilityCheckFailedSheetWithError:(NSString *)errorMessage { NSParameterAssert(errorMessage != nil); [TDCAlert alertSheetWithWindow:self.window body:TXTLS(@"TDCLicenseUpgradeEligibilitySheet[8hb-y3]", errorMessage) title:TXTLS(@"TDCLicenseUpgradeEligibilitySheet[wmk-xg]", self.licenseKey.prettyLicenseKey) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:TXTLS(@"TDCLicenseUpgradeEligibilitySheet[dn3-4r]") otherButton:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { if (buttonClicked == TDCAlertResponseAlternate) { [self actionContactSupport:nil]; } [self endSheetEarly]; }]; } - (void)_cancelEligibilityCheck { if (self.checkingEligibility == NO) { return; } [self.licenseManagerDownloader cancelRequest]; [self _checkEligibilityCompletionBlock]; } - (void)_checkEligibility { if (self.eligibility != TLOLicenseUpgradeEligibilityUnknown) { [self _eligibilityDetermined]; return; } [self _checkEligibilityOfLicense:self.licenseKey]; } - (void)_checkEligibilityOfLicense:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); if (self.checkingEligibility == NO) { self.checkingEligibility = YES; } else { return; } [self _beginProgressIndicator]; __weak TDCLicenseUpgradeEligibilitySheet *weakSelf = self; TLOLicenseManagerDownloader *licenseManagerDownloader = [TLOLicenseManagerDownloader new]; licenseManagerDownloader.completionBlock = ^(BOOL operationSuccessful, NSUInteger statusCode, id _Nullable statusContext) { [weakSelf _checkEligibilityCompletionBlock]; [weakSelf _extractEligibilityFromResponseWithStatusCode:statusCode statusContext:statusContext]; }; licenseManagerDownloader.isSilentOnFailure = YES; licenseManagerDownloader.isSilentOnSuccess = YES; [licenseManagerDownloader checkUpgradeEligibilityOfLicense:licenseKey]; self.licenseManagerDownloader = licenseManagerDownloader; } - (void)_checkEligibilityCompletionBlock { self.licenseManagerDownloader = nil; [self _endProgressIndicator]; self.checkingEligibility = NO; } - (void)_extractEligibilityFromResponseWithStatusCode:(NSUInteger)statusCode statusContext:(nullable NSDictionary *)statusContext { LogToConsoleDebug("Status code: %{public}lu", statusCode); #define _presentEligibilityCheckFailedSheet \ [self _presentEligibilityCheckFailedSheetWithError:errorMessage]; \ return; /* Check for common status codes. */ if (statusCode != 0) { NSString *errorMessage = nil; if (statusCode == TLOLicenseManagerDownloaderRequestStatusCodeServiceIsBusy) { errorMessage = TXTLS(@"TDCLicenseUpgradeEligibilitySheet[9uu-go]"); } else { errorMessage = TXTLS(@"TDCLicenseUpgradeEligibilitySheet[awy-4i]", statusCode); } _presentEligibilityCheckFailedSheet } /* There is never a time a status context should be nil for this check. */ if (statusContext == nil) { NSString *errorMessage = TXTLS(@"TDCLicenseUpgradeEligibilitySheet[z70-6s]"); _presentEligibilityCheckFailedSheet } /* Ensure the response we received is a type we support. */ id eligibilityObject = statusContext[@"licenseUpgradeEligibility"]; if (eligibilityObject == nil || [eligibilityObject isKindOfClass:[NSNumber class]] == NO) { LogToConsoleError("'licenseUpgradeEligibility' is nil or not of kind 'NSNumber'"); NSString *errorMessage = TXTLS(@"TDCLicenseUpgradeEligibilitySheet[gc5-ko]"); _presentEligibilityCheckFailedSheet } /* Save eligibility */ NSUInteger eligibility = [eligibilityObject unsignedIntegerValue]; if (eligibility != TLOLicenseUpgradeEligibilityDiscount && eligibility != TLOLicenseUpgradeEligibilityFree && eligibility != TLOLicenseUpgradeEligibilityNot && eligibility != TLOLicenseUpgradeEligibilityAlreadyUpgraded) { NSString *errorMessage = TXTLS(@"TDCLicenseUpgradeEligibilitySheet[5s6-sb]", eligibility); _presentEligibilityCheckFailedSheet } self.eligibility = eligibility; /* Present sheet */ [self _eligibilityDetermined]; /* Inform delegate */ [self.delegate upgradeEligibilitySheetChanged:self]; #undef _presentEligibilityCheckFailedSheet } - (void)_eligibilityDetermined { if (self.eligibility == TLOLicenseUpgradeEligibilityDiscount) { self.sheet = self.sheetEligibleDiscount; } else if (self.eligibility == TLOLicenseUpgradeEligibilityNot) { self.sheet = self.sheetNotEligible; } else if (self.eligibility == TLOLicenseUpgradeEligibilityFree || self.eligibility == TLOLicenseUpgradeEligibilityAlreadyUpgraded) { self.sheet = self.sheetEligibleFree; } [super startSheet]; } - (void)actionContactSupport:(id)sender { [self.delegate upgradeEligibilitySheetContactSupport:self]; } - (void)actionActivateLicense:(id)sender { [self.delegate upgradeEligibilitySheetActivateLicense:self]; } - (void)actionPurchaseUpgrade:(id)sender { [self.delegate upgradeEligibilitySheetPurchaseUpgrade:self]; } - (void)actionPurchaseStandalone:(id)sender { [self.delegate upgradeEligibilitySheetPurchaseStandalone:self]; } - (void)actionClose:(id)sender { [self endSheet]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self.delegate upgradeEligibilitySheetWillClose:self]; } @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Preferences/TDCPreferencesController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSViewHelper.h" #import "TXMasterController.h" #import "TXMenuController.h" #import "TPCPathInfoPrivate.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesReload.h" #import "TPCPreferencesUserDefaults.h" #import "TPCThemeControllerPrivate.h" #import "THOPluginManagerPrivate.h" #import "IRC.h" #import "IRCClientConfig.h" #import "IRCClient.h" #import "IRCConnectionConfig.h" #import "IRCWorld.h" #import "TLOEncryptionManagerPrivate.h" #import "TLOLocalization.h" #import "TLOpenLink.h" #import "TVCMainWindowPrivate.h" #import "TVCLogControllerInlineMediaServicePrivate.H" #import "TVCNotificationConfigurationViewControllerPrivate.h" #import "TDCAlert.h" #import "TDCFileTransferDialogPrivate.h" #import "TDCPreferencesNotificationConfigurationPrivate.h" #import "TDCPreferencesUserStyleSheetPrivate.h" #import "TDCPreferencesControllerPrivate.h" #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 #import #endif NS_ASSUME_NONNULL_BEGIN #define _scrollbackSaveLinesMin 100 #define _scrollbackSaveLinesMax 50000 #define _scrollbackVisibleLinesMin 100 #define _scrollbackVisibleLinesMax 15000 #define _inlineMediaWidthMax 2000 #define _inlineMediaWidthMin 40 #define _inlineMediaHeightMax 6000 #define _inlineMediaHeightMin 0 #define _fileTransferPortRangeMin 1024 #define _fileTransferPortRangeMax TXMaximumTCPPort #define _toolbarItemIndexGeneral 101 #define _toolbarItemIndexHighlights 104 #define _toolbarItemIndexNotifications 103 #define _toolbarItemIndexBehavior 102 #define _toolbarItemIndexControls 107 #define _toolbarItemIndexInterface 105 #define _toolbarItemIndexStyle 106 #define _toolbarItemIndexAddons 109 #define _toolbarItemIndexAdvanced 108 #define _toolbarItemIndexChannelManagement 108000 #define _toolbarItemIndexCommandScope 108001 #define _toolbarItemIndexFloodControl 108002 #define _toolbarItemIndexIncomingData 108003 #define _toolbarItemIndexCompatibility 108004 #define _toolbarItemIndexFileTransfers 108005 #define _toolbarItemIndexInlineMedia 108006 #define _toolbarItemIndexLogLocation 108007 #define _toolbarItemIndexDefaultIdentity 108008 #define _toolbarItemIndexDefaultIRCopMessages 108009 #define _toolbarItemIndexOffRecordMessaging 108010 #define _toolbarItemIndexHiddenPreferences 108011 // unused #define _addonsToolbarInstalledAddonsMenuItemIndex 109000 #define _addonsToolbarItemMultiplier 995 #define _unsignedIntegerString(_value_) [NSString stringWithUnsignedInteger:_value_] @interface TDCPreferencesController () @property (nonatomic, strong) IBOutlet NSArrayController *excludeKeywordsArrayController; @property (nonatomic, strong) IBOutlet NSArrayController *highlightKeywordsArrayController; @property (nonatomic, strong) IBOutlet NSArrayController *installedScriptsController; @property (nonatomic, weak) IBOutlet NSButton *addExcludeKeywordButton; @property (nonatomic, weak) IBOutlet NSButton *highlightNicknameButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *themeSelectionButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *transcriptFolderButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *fileTransferDownloadDestinationButton; @property (nonatomic, weak) IBOutlet NSTableView *excludeKeywordsTable; @property (nonatomic, weak) IBOutlet NSTableView *installedScriptsTable; @property (nonatomic, weak) IBOutlet NSTableView *highlightKeywordsTable; @property (nonatomic, weak) IBOutlet NSTextField *fileTransferManuallyEnteredIPAddressTextField; @property (nonatomic, strong) IBOutlet NSView *contentViewGeneral; @property (nonatomic, strong) IBOutlet NSView *contentViewHighlights; @property (nonatomic, strong) IBOutlet NSView *contentViewNotifications; @property (nonatomic, strong) IBOutlet NSView *contentViewBehavior; @property (nonatomic, strong) IBOutlet NSView *contentViewControls; @property (nonatomic, strong) IBOutlet NSView *contentViewInterface; @property (nonatomic, strong) IBOutlet NSView *contentViewStyle; @property (nonatomic, strong) IBOutlet NSView *contentViewInstalledAddons; @property (nonatomic, strong) IBOutlet NSView *contentViewChannelManagement; @property (nonatomic, strong) IBOutlet NSView *contentViewCommandScope; @property (nonatomic, strong) IBOutlet NSView *contentViewCompatibility; @property (nonatomic, strong) IBOutlet NSView *contentViewFloodControl; @property (nonatomic, strong) IBOutlet NSView *contentViewIncomingData; @property (nonatomic, strong) IBOutlet NSView *contentViewFileTransfers; @property (nonatomic, strong) IBOutlet NSView *contentViewInlineMedia; @property (nonatomic, strong) IBOutlet NSView *contentViewLogLocation; @property (nonatomic, strong) IBOutlet NSView *contentViewDefaultIdentity; @property (nonatomic, strong) IBOutlet NSView *contentViewDefaultIRCopMessages; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 @property (nonatomic, strong) IBOutlet NSView *contentViewOffRecordMessaging; #endif @property (nonatomic, strong) IBOutlet NSView *contentViewHiddenPreferences; @property (nonatomic, weak) IBOutlet NSButton *checkForUpdatesDontCheck; @property (nonatomic, weak) IBOutlet NSButton *checkForUpdatesAutomaticallyCheck; @property (nonatomic, weak) IBOutlet NSButton *checkForUpdatesAutomaticallyDownload; @property (nonatomic, weak) IBOutlet NSButton *forwardNoticeToServerConsoleButton; @property (nonatomic, weak) IBOutlet NSButton *forwardNoticeToSelectedChannelButton; @property (nonatomic, weak) IBOutlet NSButton *forwardNoticeToQueryButton; @property (nonatomic, weak) IBOutlet NSButton *inlineMediaEnabledButton; @property (nonatomic, weak) IBOutlet NSStackView *contentViewGeneralStackView; @property (nonatomic, weak) IBOutlet NSView *contentViewGeneralCheckForUpdatesView; @property (nonatomic, weak) IBOutlet NSView *contentViewGeneralShareDataView; @property (nonatomic, strong) IBOutlet NSToolbar *navigationToolbar; @property (nonatomic, strong) IBOutlet NSMenu *installedAddonsMenu; @property (nonatomic, assign) BOOL reloadingTheme; @property (nonatomic, assign) BOOL reloadingThemeBySelection; @property (nonatomic, weak) IBOutlet NSView *notificationControllerHostView; @property (nonatomic, strong) IBOutlet TVCNotificationConfigurationViewController *notificationController; @property (nonatomic, strong) TDCPreferencesUserStyleSheet *userStyleSheet; - (IBAction)onAddExcludeKeyword:(id)sender; - (IBAction)onAddHighlightKeyword:(id)sender; // changed - (IBAction)onChangedAppearance:(id)sender; - (IBAction)onChangedCheckForUpdates:(id)sender; - (IBAction)onChangedCheckForBetaUpdates:(id)sender; - (IBAction)onChangedChannelViewArrangement:(id)sender; - (IBAction)onChangedDisableNicknameColorHashing:(id)sender; - (IBAction)onChangedForwardNoticeTo:(id)sender; - (IBAction)onChangedHighlightLogging:(id)sender; - (IBAction)onChangedHighlightType:(id)sender; - (IBAction)onChangedInlineMediaOption:(id)sender; - (IBAction)onChangedInputHistoryScheme:(id)sender; - (IBAction)onChangedMainInputTextViewFontSize:(id)sender; // changed - (IBAction)onChangedMainWindowSegmentedController:(id)sender; - (IBAction)onChangedScrollbackSaveLimit:(id)sender; - (IBAction)onChangedScrollbackVisibleLimit:(id)sender; - (IBAction)onChangedServerListUnreadBadgeColor:(id)sender; - (IBAction)onChangedTheme:(id)sender; - (IBAction)onChangedThemeSelection:(id)sender; // changed - (IBAction)onChangedTranscriptFolder:(id)sender; - (IBAction)onChangedTransparency:(id)sender; - (IBAction)onChangedUserListModeColor:(id)sender; - (IBAction)onChangedUserListModeSortOrder:(id)sender; - (IBAction)onFileTransferDownloadDestinationFolderChanged:(id)sender; - (IBAction)onFileTransferIPAddressDetectionMethodChanged:(id)sender; - (IBAction)onModifyUserStyleSheetRules:(id)sender; - (IBAction)onOpenPathToScripts:(id)sender; - (IBAction)onOpenPathToTheme:(id)sender; // changed - (IBAction)onPrefPaneSelected:(id)sender; - (IBAction)onResetServerListUnreadBadgeColorsToDefault:(id)sender; - (IBAction)onResetUserListModeColorsToDefaults:(id)sender; - (IBAction)onSelectNewFont:(id)sender; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (IBAction)offRecordMessagingPolicyChanged:(id)sender; - (IBAction)offRecordMessagingOpenOfficialWebsite:(id)sender; - (IBAction)offRecordMessagingOpenHelpDocument:(id)sender; #endif @end @implementation TDCPreferencesController - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCPreferences" owner:self topLevelObjects:nil]; } - (void)awakeFromNib { [super awakeFromNib]; NSMutableArray *notifications = [NSMutableArray array]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeAddressBookMatch]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeConnect]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeDisconnect]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeHighlight]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeInvite]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeKick]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeChannelMessage]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeChannelNotice]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeNewPrivateMessage]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypePrivateMessage]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypePrivateNotice]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeUserJoined]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeUserParted]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeUserDisconnected]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeFileTransferReceiveRequested]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeFileTransferSendSuccessful]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeFileTransferReceiveSuccessful]]; [notifications addObject:@" "]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeFileTransferSendFailed]]; [notifications addObject:[TDCPreferencesNotificationConfiguration objectWithEventType:TXNotificationTypeFileTransferReceiveFailed]]; self.notificationController.notifications = notifications; [self.notificationController attachToView:self.notificationControllerHostView]; [self setUpToolbarItemsAndMenus]; [self updateCheckForUpdatesMatrix]; [self updateFileTransferDownloadDestinationFolder]; [self updateForwardNoticeToMatrix]; [self updateInlineMediaEnabled]; [self updateThemeSelection]; [self updateTranscriptFolder]; [self onChangedHighlightType:nil]; [self onFileTransferIPAddressDetectionMethodChanged:nil]; self.installedScriptsTable.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"string" ascending:YES selector:@selector(caseInsensitiveCompare:)] ]; [RZNotificationCenter() addObserver:self selector:@selector(onThemeListDidChange:) name:TPCThemeControllerThemeListDidChangeNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(onThemeWillReload:) name:TVCMainWindowWillReloadThemeNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(onThemeReloadComplete:) name:TVCMainWindowDidReloadThemeNotification object:nil]; #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 0 /* Hide preferences for updates when support is not enabled. */ [self.contentViewGeneralStackView setVisibilityPriority:NSStackViewVisibilityPriorityNotVisible forView:self.contentViewGeneralCheckForUpdatesView]; #endif [self.contentViewGeneral layoutSubtreeIfNeeded]; [self restoreWindowFrame]; } #pragma mark - #pragma mark Utilities - (void)show { [self show:TDCPreferencesControllerSelectionDefault]; } - (void)show:(TDCPreferencesControllerSelection)selection { switch (selection) { case TDCPreferencesControllerSelectionNotifications: { [self _showPane:self.contentViewNotifications selectedItem:_toolbarItemIndexNotifications]; break; } case TDCPreferencesControllerSelectionStyle: { [self _showPane:self.contentViewStyle selectedItem:_toolbarItemIndexStyle]; break; } case TDCPreferencesControllerSelectionHiddenPreferences: { [self _showPane:self.contentViewHiddenPreferences selectedItem:_toolbarItemIndexAdvanced]; break; } default: { [self _showPane:self.contentViewGeneral selectedItem:_toolbarItemIndexGeneral]; break; } } } - (void)_showPane:(NSView *)view selectedItem:(NSInteger)selectedItem { [self firstPane:view selectedItem:selectedItem]; [super show]; } #pragma mark - #pragma mark NSToolbar Delegates - (void)setUpToolbarItemsAndMenus { NSArray *plugins = sharedPluginManager().pluginsWithPreferencePanes; if (plugins.count > 0) { [self.installedAddonsMenu addItem:[NSMenuItem separatorItem]]; } [plugins enumerateObjectsUsingBlock:^(THOPluginItem *plugin, NSUInteger index, BOOL *stop) { NSUInteger tagIndex = (index + _addonsToolbarItemMultiplier); NSMenuItem *pluginMenu = [NSMenuItem menuItemWithTitle:plugin.pluginPreferencesPaneMenuItemTitle target:self action:@selector(onPrefPaneSelected:)]; pluginMenu.tag = tagIndex; [self.installedAddonsMenu addItem:pluginMenu]; }]; } - (void)onPrefPaneSelected:(id)sender { #define _de(matchTag, view, selectionIndex) \ case (matchTag): { \ [self firstPane:(view) selectedItem:(selectionIndex)]; \ break; \ } switch ([sender tag]) { _de(_toolbarItemIndexGeneral, self.contentViewGeneral, _toolbarItemIndexGeneral) _de(_toolbarItemIndexHighlights, self.contentViewHighlights, _toolbarItemIndexHighlights) _de(_toolbarItemIndexNotifications, self.contentViewNotifications, _toolbarItemIndexNotifications) _de(_toolbarItemIndexBehavior, self.contentViewBehavior, _toolbarItemIndexBehavior) _de(_toolbarItemIndexControls, self.contentViewControls, _toolbarItemIndexControls) _de(_toolbarItemIndexInterface, self.contentViewInterface, _toolbarItemIndexInterface) _de(_toolbarItemIndexStyle, self.contentViewStyle, _toolbarItemIndexStyle) _de(_toolbarItemIndexChannelManagement, self.contentViewChannelManagement, _toolbarItemIndexAdvanced) _de(_toolbarItemIndexCommandScope, self.contentViewCommandScope, _toolbarItemIndexAdvanced) _de(_toolbarItemIndexCompatibility, self.contentViewCompatibility, _toolbarItemIndexAdvanced) _de(_toolbarItemIndexFloodControl, self.contentViewFloodControl, _toolbarItemIndexAdvanced) _de(_toolbarItemIndexIncomingData, self.contentViewIncomingData, _toolbarItemIndexAdvanced) _de(_toolbarItemIndexFileTransfers, self.contentViewFileTransfers, _toolbarItemIndexAdvanced) _de(_toolbarItemIndexInlineMedia, self.contentViewInlineMedia, _toolbarItemIndexAdvanced) _de(_toolbarItemIndexLogLocation, self.contentViewLogLocation, _toolbarItemIndexAdvanced); _de(_toolbarItemIndexDefaultIdentity, self.contentViewDefaultIdentity, _toolbarItemIndexAdvanced) _de(_toolbarItemIndexDefaultIRCopMessages, self.contentViewDefaultIRCopMessages, _toolbarItemIndexAdvanced) #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 _de(_toolbarItemIndexOffRecordMessaging, self.contentViewOffRecordMessaging, _toolbarItemIndexAdvanced) #endif _de(_addonsToolbarInstalledAddonsMenuItemIndex, self.contentViewInstalledAddons, _toolbarItemIndexAddons) default: { if ([sender tag] < _addonsToolbarItemMultiplier) { break; } NSUInteger pluginIndex = ([sender tag] - _addonsToolbarItemMultiplier); THOPluginItem *plugin = sharedPluginManager().pluginsWithPreferencePanes[pluginIndex]; NSView *preferencesView = plugin.pluginPreferencesPaneView; [self firstPane:preferencesView selectedItem:_toolbarItemIndexAddons]; break; } } #undef _de } - (void)firstPane:(NSView *)view selectedItem:(NSInteger)selectedItem { [self.window replaceContentView:view]; if (selectedItem >= 0) { self.navigationToolbar.selectedItemIdentifier = _unsignedIntegerString(selectedItem); } else { self.navigationToolbar.selectedItemIdentifier = nil; } } - (void)restoreWindowFrame { NSWindow *window = self.window; [window saveSizeAsDefault]; [window restoreWindowStateForClass:self.class]; } - (void)saveWindowFrame { NSWindow *window = self.window; [window restoreDefaultSizeAndDisplay:NO]; [window saveWindowStateForClass:self.class]; } #pragma mark - #pragma mark KVC Properties - (NSArray *)installedScripts { NSMutableArray *scriptsInstalled = [NSMutableArray array]; [scriptsInstalled addObjectsFromArray:sharedPluginManager().supportedAppleScriptCommands]; [scriptsInstalled addObjectsFromArray:sharedPluginManager().supportedUserInputCommands]; return scriptsInstalled.stringArrayControllerObjects; } - (NSString *)scrollbackSaveLimit { return _unsignedIntegerString([TPCPreferences scrollbackSaveLimit]); } - (void)setScrollbackSaveLimit:(NSString *)value { [TPCPreferences setScrollbackSaveLimit:value.integerValue]; } - (NSString *)scrollbackVisibleLimit { return _unsignedIntegerString([TPCPreferences scrollbackVisibleLimit]); } - (void)setScrollbackVisibleLimit:(NSString *)value { [TPCPreferences setScrollbackVisibleLimit:value.integerValue]; } - (NSString *)completionSuffix { return [TPCPreferences tabCompletionSuffix]; } - (void)setCompletionSuffix:(NSString *)value { [TPCPreferences setTabCompletionSuffix:value]; } - (NSString *)inlineMediaMaxWidth { return _unsignedIntegerString([TPCPreferences inlineMediaMaxWidth]); } - (NSString *)inlineMediaMaxHeight { return _unsignedIntegerString([TPCPreferences inlineMediaMaxHeight]); } - (void)setInlineMediaMaxWidth:(NSString *)value { [TPCPreferences setInlineMediaMaxWidth:value.integerValue]; } - (void)setInlineMediaMaxHeight:(NSString *)value { [TPCPreferences setInlineMediaMaxHeight:value.integerValue]; } - (NSString *)themeChannelViewFontName { NSFont *currentFont = [TPCPreferences themeChannelViewFont]; return currentFont.displayName; } - (CGFloat)themeChannelViewFontSize { return [TPCPreferences themeChannelViewFontSize]; } - (void)setThemeChannelViewFontName:(NSString *)value { return; } - (void)setThemeChannelViewFontSize:(CGFloat)value { return; } - (NSString *)fileTransferPortRangeStart { return _unsignedIntegerString([TPCPreferences fileTransferPortRangeStart]); } - (NSString *)fileTransferPortRangeEnd { return _unsignedIntegerString([TPCPreferences fileTransferPortRangeEnd]); } - (void)setFileTransferPortRangeStart:(NSString *)value { [TPCPreferences setFileTransferPortRangeStart:value.integerValue]; } - (void)setFileTransferPortRangeEnd:(NSString *)value { [TPCPreferences setFileTransferPortRangeEnd:value.integerValue]; } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (void)setTextEncryptionIsOpportunistic:(BOOL)textEncryptionIsOpportunistic { [TPCPreferences setTextEncryptionIsOpportunistic:textEncryptionIsOpportunistic]; } - (BOOL)textEncryptionIsOpportunistic { if ([TPCPreferences textEncryptionIsEnabled] == NO) { return NO; } if ([TPCPreferences textEncryptionIsRequired]) { return YES; } return [TPCPreferences textEncryptionIsOpportunistic]; } - (BOOL)textEncryptionIsOpportunisticPreferenceEnabled { return ([TPCPreferences textEncryptionIsEnabled] && [TPCPreferences textEncryptionIsRequired] == NO); } - (void)setTextEncryptionIsRequired:(BOOL)textEncryptionIsRequired { [TPCPreferences setTextEncryptionIsRequired:textEncryptionIsRequired]; [self willChangeValueForKey:@"textEncryptionIsOpportunistic"]; [self didChangeValueForKey:@"textEncryptionIsOpportunistic"]; } - (BOOL)textEncryptionIsRequired { if ([TPCPreferences textEncryptionIsEnabled] == NO) { return NO; } return [TPCPreferences textEncryptionIsRequired]; } - (BOOL)textEncryptionIsRequiredPreferenceEnabled { return [TPCPreferences textEncryptionIsEnabled]; } - (void)setTextEncryptionIsEnabled:(BOOL)textEncryptionIsEnabled { [TPCPreferences setTextEncryptionIsEnabled:textEncryptionIsEnabled]; [self willChangeValueForKey:@"textEncryptionIsOpportunistic"]; [self willChangeValueForKey:@"textEncryptionIsOpportunisticPreferenceEnabled"]; [self willChangeValueForKey:@"textEncryptionIsRequired"]; [self willChangeValueForKey:@"textEncryptionIsRequiredPreferenceEnabled"]; [self didChangeValueForKey:@"textEncryptionIsOpportunistic"]; [self didChangeValueForKey:@"textEncryptionIsOpportunisticPreferenceEnabled"]; [self didChangeValueForKey:@"textEncryptionIsRequired"]; [self didChangeValueForKey:@"textEncryptionIsRequiredPreferenceEnabled"]; } - (BOOL)textEncryptionIsEnabled { return [TPCPreferences textEncryptionIsEnabled]; } #else - (void)setTextEncryptionIsOpportunistic:(BOOL)textEncryptionIsOpportunistic { } - (BOOL)textEncryptionIsOpportunistic { } - (BOOL)textEncryptionIsOpportunisticPreferenceEnabled { } - (void)setTextEncryptionIsRequired:(BOOL)textEncryptionIsRequired { } - (BOOL)textEncryptionIsRequired { } - (BOOL)textEncryptionIsRequiredPreferenceEnabled { } - (void)setTextEncryptionIsEnabled:(BOOL)textEncryptionIsEnabled { } - (BOOL)textEncryptionIsEnabled { } #endif - (BOOL)highlightCurrentNickname { if ([TPCPreferences highlightMatchingMethod] == TXNicknameHighlightMatchTypeRegularExpression) { return NO; } return [TPCPreferences highlightCurrentNickname]; } - (void)setHighlightCurrentNickname:(BOOL)value { [TPCPreferences setHighlightCurrentNickname:value]; } - (BOOL)appNapEnabled { return [TPCPreferences appNapEnabled]; } - (void)setAppNapEnabled:(BOOL)appNapEnabled { [TPCPreferences setAppNapEnabled:appNapEnabled]; } - (BOOL)onlySpeakEventsForSelection { return [TPCPreferences onlySpeakEventsForSelection]; } - (void)setOnlySpeakEventsForSelection:(BOOL)onlySpeakEventsForSelection { [TPCPreferences setOnlySpeakEventsForSelection:onlySpeakEventsForSelection]; [self willChangeValueForKey:@"channelMessageSpeakChannelName"]; [self didChangeValueForKey:@"channelMessageSpeakChannelName"]; } - (BOOL)channelMessageSpeakChannelName { if ([TPCPreferences onlySpeakEventsForSelection]) { return NO; } return [TPCPreferences channelMessageSpeakChannelName]; } - (void)setChannelMessageSpeakChannelName:(BOOL)channelMessageSpeakChannelName { [TPCPreferences setChannelMessageSpeakChannelName:channelMessageSpeakChannelName]; } - (BOOL)channelMessageSpeakNickname { return [TPCPreferences channelMessageSpeakNickname]; } - (void)setChannelMessageSpeakNickname:(BOOL)channelMessageSpeakNickname { [TPCPreferences setChannelMessageSpeakNickname:channelMessageSpeakNickname]; } - (NSColor *)serverListUnreadCountBadgeHighlightColor { NSColor *value = [RZUserDefaults() colorForKey:@"Server List Unread Message Count Badge Colors -> Highlight"]; if (value == nil) { value = [NSColor clearColor]; } return value; } - (void)setServerListUnreadCountBadgeHighlightColor:(NSColor *)serverListUnreadCountBadgeHighlightColor { NSColor *newValue = serverListUnreadCountBadgeHighlightColor; if ([newValue isEqual:[NSColor clearColor]]) { newValue = nil; } [RZUserDefaults() setColor:newValue forKey:@"Server List Unread Message Count Badge Colors -> Highlight"]; } - (NSColor *)userListNoModeColor { NSColor *value = [RZUserDefaults() colorForKey:@"User List Mode Badge Colors -> no mode"]; if (value == nil) { value = [NSColor clearColor]; } return value; } - (void)setUserListNoModeColor:(NSColor *)userListNoModeColor { NSColor *newValue = userListNoModeColor; if ([newValue isEqual:[NSColor clearColor]]) { newValue = nil; } [RZUserDefaults() setColor:newValue forKey:@"User List Mode Badge Colors -> no mode"]; } - (BOOL)logTranscript { return [TPCPreferences logToDisk]; } - (void)setLogTranscript:(BOOL)logTranscript { [TPCPreferences setLogToDisk:logTranscript]; } - (BOOL)inlineMediaLimitToBasics { return [TPCPreferences inlineMediaLimitToBasics]; } - (void)setInlineMediaLimitToBasics:(BOOL)inlineMediaLimitToBasics { [TPCPreferences setInlineMediaLimitToBasics:inlineMediaLimitToBasics]; [self willChangeValueForKey:@"inlineMediaLimitBasicsToFiles"]; [self didChangeValueForKey:@"inlineMediaLimitBasicsToFiles"]; } - (BOOL)inlineMediaLimitBasicsToFiles { /* Show value as enabled when basics is disabled */ if ([TPCPreferences inlineMediaLimitToBasics] == NO) { return NO; // UI negates bool so return NO for YES } return [TPCPreferences inlineMediaLimitBasicsToFiles]; } - (void)setInlineMediaLimitBasicsToFiles:(BOOL)inlineMediaLimitBasicsToFiles { [TPCPreferences setInlineMediaLimitBasicsToFiles:inlineMediaLimitBasicsToFiles]; } - (BOOL)validateValue:(inout id *)value forKey:(NSString *)key error:(out NSError **)outError { if ([key isEqualToString:@"scrollbackSaveLimit"]) { NSInteger valueInteger = [*value integerValue]; if (valueInteger < _scrollbackSaveLinesMin) { *value = _unsignedIntegerString(_scrollbackSaveLinesMin); } else if (valueInteger > _scrollbackSaveLinesMax) { *value = _unsignedIntegerString(_scrollbackSaveLinesMax); } } else if ([key isEqualToString:@"scrollbackVisibleLimit"]) { NSInteger valueInteger = [*value integerValue]; if (valueInteger < _scrollbackVisibleLinesMin && valueInteger != 0) { *value = _unsignedIntegerString(_scrollbackVisibleLinesMin); } else if (valueInteger > _scrollbackVisibleLinesMax) { *value = _unsignedIntegerString(_scrollbackVisibleLinesMax); } } else if ([key isEqualToString:@"inlineMediaMaxWidth"]) { NSInteger valueInteger = [*value integerValue]; if (valueInteger < _inlineMediaWidthMin) { *value = _unsignedIntegerString(_inlineMediaWidthMin); } else if (_inlineMediaWidthMax < valueInteger) { *value = _unsignedIntegerString(_inlineMediaWidthMax); } } else if ([key isEqualToString:@"inlineMediaMaxHeight"]) { NSInteger valueInteger = [*value integerValue]; if (valueInteger < _inlineMediaHeightMin) { *value = _unsignedIntegerString(_inlineMediaHeightMin); } else if (_inlineMediaHeightMax < valueInteger) { *value = _unsignedIntegerString(_inlineMediaHeightMax); } } else if ([key isEqualToString:@"fileTransferPortRangeStart"]) { NSInteger valueInteger = [*value integerValue]; NSUInteger valueRangeEnd = [TPCPreferences fileTransferPortRangeEnd]; if (valueInteger < _fileTransferPortRangeMin) { *value = _unsignedIntegerString(_fileTransferPortRangeMin); } else if (_fileTransferPortRangeMax < valueInteger) { *value = _unsignedIntegerString(_fileTransferPortRangeMax); } valueInteger = [*value integerValue]; if (valueInteger > valueRangeEnd) { *value = _unsignedIntegerString(valueRangeEnd); } } else if ([key isEqualToString:@"fileTransferPortRangeEnd"]) { NSInteger valueInteger = [*value integerValue]; NSUInteger valueRangeStart = [TPCPreferences fileTransferPortRangeStart]; if (valueInteger < _fileTransferPortRangeMin) { *value = _unsignedIntegerString(_fileTransferPortRangeMin); } else if (_fileTransferPortRangeMax < valueInteger) { *value = _unsignedIntegerString(_fileTransferPortRangeMax); } valueInteger = [*value integerValue]; if (valueInteger < valueRangeStart) { *value = _unsignedIntegerString(valueRangeStart); } } return YES; } #pragma mark - #pragma mark File Transfer Destination Folder Popup - (void)updateFileTransferDownloadDestinationFolder { TDCFileTransferDialog *transferController = [TXSharedApplication sharedFileTransferDialog]; NSURL *path = transferController.downloadDestinationURL; NSMenuItem *item = [self.fileTransferDownloadDestinationButton itemAtIndex:0]; if (path == nil) { item.image = nil; item.title = TXTLS(@"TDCPreferencesController[721-ie]"); } else { NSImage *icon = [RZWorkspace() iconForFile:path.path]; item.image = icon; icon.size = NSMakeSize(16, 16); item.title = path.lastPathComponent; } } - (void)onFileTransferDownloadDestinationFolderChanged:(id)sender { TDCFileTransferDialog *transferController = [TXSharedApplication sharedFileTransferDialog]; if (self.fileTransferDownloadDestinationButton.selectedTag == 2) { NSOpenPanel *d = [NSOpenPanel openPanel]; d.allowsMultipleSelection = NO; d.canChooseDirectories = YES; d.canChooseFiles = NO; d.canCreateDirectories = YES; d.resolvesAliases = YES; d.prompt = TXTLS(@"Prompts[xne-79]"); [d beginSheetModalForWindow:self.window completionHandler:^(NSInteger returnCode) { [self.fileTransferDownloadDestinationButton selectItemAtIndex:0]; if (returnCode != NSModalResponseOK) { return; } NSURL *path = d.URLs[0]; NSError *bookmarkError = nil; NSData *bookmark = [path bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&bookmarkError]; if (bookmark == nil) { LogToConsoleError("Error creating bookmark for URL ('%{public}@'): %{public}@", path.standardizedTildePath, bookmarkError.localizedDescription); } [transferController setDownloadDestinationURL:bookmark]; [self updateFileTransferDownloadDestinationFolder]; }]; } else if (self.fileTransferDownloadDestinationButton.selectedTag == 3) { [self.fileTransferDownloadDestinationButton selectItemAtIndex:0]; [transferController setDownloadDestinationURL:nil]; [self updateFileTransferDownloadDestinationFolder]; } } #pragma mark - #pragma mark Transcript Folder Popup - (void)updateTranscriptFolder { NSURL *path = [TPCPathInfo transcriptFolderURL]; NSMenuItem *item = [self.transcriptFolderButton itemAtIndex:0]; if (path == nil) { item.image = nil; item.title = TXTLS(@"TDCPreferencesController[70s-c6]"); } else { NSImage *icon = [RZWorkspace() iconForFile:path.path]; item.image = icon; icon.size = NSMakeSize(16, 16); item.title = path.lastPathComponent; } } - (void)onChangedTranscriptFolder:(id)sender { if (self.transcriptFolderButton.selectedTag == 2) { NSOpenPanel *d = [NSOpenPanel openPanel]; d.allowsMultipleSelection = NO; d.canChooseDirectories = YES; d.canChooseFiles = NO; d.canCreateDirectories = YES; d.resolvesAliases = YES; d.prompt = TXTLS(@"Prompts[xne-79]"); [d beginSheetModalForWindow:self.window completionHandler:^(NSInteger returnCode) { [self.transcriptFolderButton selectItemAtIndex:0]; if (returnCode != NSModalResponseOK) { return; } NSURL *path = d.URLs[0]; NSError *bookmarkError = nil; NSData *bookmark = [path bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&bookmarkError]; if (bookmark == nil) { LogToConsoleError("Error creating bookmark for URL ('%{public}@'): %{public}@", path.standardizedTildePath, bookmarkError.localizedDescription); return; } [self setTranscriptFolderURL:bookmark]; }]; } else if (self.transcriptFolderButton.selectedTag == 3) { [self.transcriptFolderButton selectItemAtIndex:0]; [self setTranscriptFolderURL:nil]; } } - (void)setTranscriptFolderURL:(nullable NSData *)transcriptFolderURL { [TPCPathInfo setTranscriptFolderURL:transcriptFolderURL]; [TPCPreferences performReloadAction:TPCPreferencesReloadActionLogTranscripts]; [self updateTranscriptFolder]; } #pragma mark - #pragma mark Theme - (void)updateThemeSelection { [self.themeSelectionButton removeAllItems]; NSString *currentThemeName = themeController().name; TPCThemeStorageLocation currentStorageLocation = themeController().storageLocation; [themeController() enumerateAvailableThemesWithBlock:^(NSString *themeName, TPCThemeStorageLocation storageLocation, BOOL multipleVariants, BOOL *stop) { NSString *displayName = themeName; if (multipleVariants) { displayName = [NSString stringWithFormat:@"%@ (%@)", themeName, [TPCThemeController descriptionForStorageLocation:storageLocation]]; } NSMenuItem *item = [NSMenuItem menuItemWithTitle:displayName target:nil action:nil]; item.representedObject = @{ @"themeName" : themeName, @"storageLocation" : @(storageLocation) }; if ([currentThemeName isEqualToString:themeName] && currentStorageLocation == storageLocation) { item.tag = 100; // Tag for item to select } [self.themeSelectionButton.menu addItem:item]; }]; [self.themeSelectionButton selectItemWithTag:100]; } - (void)onChangedThemeSelection:(id)sender { NSMenuItem *selectedItem = self.themeSelectionButton.selectedItem; NSDictionary *context = selectedItem.representedObject; NSString *newThemeName = context[@"themeName"]; TPCThemeStorageLocation newStorageLocation = [context unsignedIntegerForKey:@"storageLocation"]; NSString *newTheme = [TPCThemeController buildFilename:newThemeName forStorageLocation:newStorageLocation]; NSString *currentTheme = [TPCPreferences themeName]; if ([currentTheme isEqualToString:newTheme]) { return; } [TPCPreferences setThemeName:newTheme]; self.reloadingThemeBySelection = YES; [self onChangedTheme:nil]; } - (void)onChangedThemeSelectionReloadComplete:(NSNotification *)notification { NSMutableString *forcedValuesMutable = [NSMutableString string]; if ([TPCPreferences themeNicknameFormatPreferenceUserConfigurable] == NO) { [forcedValuesMutable appendString:TXTLS(@"TDCPreferencesController[77t-de]")]; [forcedValuesMutable appendString:@"\n"]; } if ([TPCPreferences themeTimestampFormatPreferenceUserConfigurable] == NO) { [forcedValuesMutable appendString:TXTLS(@"TDCPreferencesController[ddh-hr]")]; [forcedValuesMutable appendString:@"\n"]; } if ([TPCPreferences themeChannelViewFontPreferenceUserConfigurable] == NO) { [forcedValuesMutable appendString:TXTLS(@"TDCPreferencesController[we8-i8]")]; [forcedValuesMutable appendString:@"\n"]; } NSString *forcedValues = forcedValuesMutable.trim; if (forcedValues.length == 0) { return; } NSString *currentTheme = [TPCPreferences themeName]; NSString *themeName = [TPCThemeController extractThemeName:currentTheme]; [TDCAlert alertSheetWithWindow:[NSApp keyWindow] body:TXTLS(@"TDCPreferencesController[q4o-2f]", themeName, forcedValues) title:TXTLS(@"TDCPreferencesController[uc0-z7]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil otherButton:nil suppressionKey:@"theme_override_info" suppressionText:nil completionBlock:nil]; } - (void)onSelectNewFont:(id)sender { NSFont *currentFont = [TPCPreferences themeChannelViewFont]; [RZFontManager() setSelectedFont:currentFont isMultiple:NO]; [RZFontManager() orderFrontFontPanel:self]; RZFontManager().action = @selector(onChangedChannelViewFont:); } - (void)onChangedChannelViewFont:(NSFontManager *)sender { NSFont *currentFont = [TPCPreferences themeChannelViewFont]; NSFont *newFont = [sender convertFont:currentFont]; [self willChangeValueForKey:@"themeChannelViewFontName"]; [self willChangeValueForKey:@"themeChannelViewFontSize"]; [TPCPreferences setThemeChannelViewFontName:newFont.fontName]; [TPCPreferences setThemeChannelViewFontSize:newFont.pointSize]; [self didChangeValueForKey:@"themeChannelViewFontName"]; [self didChangeValueForKey:@"themeChannelViewFontSize"]; [self onChangedTheme:nil]; } - (void)onChangedTransparency:(id)sender { [mainWindow() updateAlphaValueToReflectPreferences]; } #pragma mark - #pragma mark User Style Sheet Rules - (void)onModifyUserStyleSheetRules:(id)sender { TDCPreferencesUserStyleSheet *sheet = [[TDCPreferencesUserStyleSheet alloc] initWithWindow:self.window]; sheet.delegate = (id)self; [sheet start]; self.userStyleSheet = sheet; } - (void)userStyleSheetRulesChanged:(TDCPreferencesUserStyleSheet *)sender { [self onChangedTheme:nil]; } - (void)userStyleSheetWillClose:(TDCPreferencesUserStyleSheet *)sender { self.userStyleSheet = nil; } #pragma mark - #pragma mark Forward Notice To - (void)updateForwardNoticeToMatrix { TXNoticeSendLocation location = [TPCPreferences locationToSendNotices]; self.forwardNoticeToServerConsoleButton.state = (location == TXNoticeSendLocationServerConsole); self.forwardNoticeToSelectedChannelButton.state = (location == TXNoticeSendLocationSelectedChannel); self.forwardNoticeToQueryButton.state = (location == TXNoticeSendLocationQuery); } - (void)onChangedForwardNoticeTo:(id)sender { [TPCPreferences setLocationToSendNotices:[sender tag]]; } #pragma mark - #pragma mark Updates - (void)updateCheckForUpdatesMatrix { #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 SPUUpdater *updater = masterController().updateController.updater; self.checkForUpdatesAutomaticallyDownload.state = updater.automaticallyDownloadsUpdates; self.checkForUpdatesAutomaticallyCheck.state = updater.automaticallyChecksForUpdates; self.checkForUpdatesDontCheck.state = (updater.automaticallyDownloadsUpdates == NO && updater.automaticallyChecksForUpdates == NO); #endif } - (void)onChangedCheckForUpdates:(id)sender { #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 SPUUpdater *updater = masterController().updateController.updater; updater.automaticallyChecksForUpdates = (self.checkForUpdatesAutomaticallyCheck.state == NSControlStateValueOn); updater.automaticallyDownloadsUpdates = (self.checkForUpdatesAutomaticallyDownload.state == NSControlStateValueOn); #endif } - (void)onChangedCheckForBetaUpdates:(id)sender { #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 [TPCPreferences performReloadAction:TPCPreferencesReloadActionSparkleFrameworkFeedURL]; if ([TPCPreferences receiveBetaUpdates]) { [menuController() checkForUpdates:nil]; } #endif } #pragma mark - #pragma mark Actions - (void)onChangedDisableNicknameColorHashing:(id)sender { [self onChangedTheme:nil]; } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (void)offRecordMessagingPolicyChanged:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionEncryptionPolicy]; } - (void)offRecordMessagingOpenOfficialWebsite:(id)sender { [TLOpenLink openWithString:@"https://otr.cypherpunks.ca/"]; } - (void)offRecordMessagingOpenHelpDocument:(id)sender { [TLOpenLink openWithString:@"https://help.codeux.com/textual/Off-the-Record-Messaging.kb"]; } #endif - (void)onChangedHighlightType:(id)sender { [self willChangeValueForKey:@"highlightCurrentNickname"]; [self didChangeValueForKey:@"highlightCurrentNickname"]; if ([TPCPreferences highlightMatchingMethod] == TXNicknameHighlightMatchTypeRegularExpression) { self.highlightNicknameButton.enabled = NO; } else { self.highlightNicknameButton.enabled = YES; } } - (void)editTableView:(NSTableView *)tableView { NSInteger rowSelection = (tableView.numberOfRows - 1); [tableView scrollRowToVisible:rowSelection]; [tableView editColumn:0 row:rowSelection withEvent:nil select:YES]; } - (void)onAddHighlightKeyword:(id)sender { [self.highlightKeywordsArrayController add:nil]; XRPerformBlockAsynchronouslyOnMainQueue(^{ [self editTableView:self.highlightKeywordsTable]; }); } - (void)onAddExcludeKeyword:(id)sender { [self.excludeKeywordsArrayController add:nil]; XRPerformBlockAsynchronouslyOnMainQueue(^{ [self editTableView:self.excludeKeywordsTable]; }); } + (void)openProxySettingsInSystemPreferences { AEDesc aeDesc = { typeNull, NULL }; OSStatus aeDescStatus = AECreateDesc('ptru', "Proxies", 7, &aeDesc); if (aeDescStatus != noErr) { LogToConsoleError("aeDescStatus returned value other than noErr: %{public}i", aeDescStatus); return; } NSURL *prefPaneURL = [NSURL fileURLWithPath:@"/System/Library/PreferencePanes/Network.prefPane"]; LSLaunchURLSpec launchSpec = { 0 }; launchSpec.appURL = NULL; launchSpec.asyncRefCon = NULL; launchSpec.itemURLs = (__bridge CFArrayRef)@[prefPaneURL]; launchSpec.launchFlags = (kLSLaunchAsync | kLSLaunchDontAddToRecents); launchSpec.passThruParams = &aeDesc; (void)LSOpenFromURLSpec(&launchSpec, NULL); } - (void)updateInlineMediaEnabled { if ([TPCPreferences showInlineMedia]) { self.inlineMediaEnabledButton.state = NSControlStateValueOn; } else { self.inlineMediaEnabledButton.state = NSControlStateValueOff; } } - (void)onChangedInlineMediaOption:(id)sender { if (self.inlineMediaEnabledButton.state == NSControlStateValueOff) { [TPCPreferences setShowInlineMedia:NO]; [self onChangedTheme:nil]; return; } [TVCLogControllerInlineMediaService askPermissionToEnableInlineMediaWithCompletionBlock:^(BOOL granted) { if (granted) { [TPCPreferences setShowInlineMedia:YES]; [self onChangedTheme:nil]; } else { self.inlineMediaEnabledButton.state = NSControlStateValueOff; } }]; } - (void)onResetUserListModeColorsToDefaults:(id)sender { [RZUserDefaults() setObject:nil forKey:@"User List Mode Badge Colors -> +y"]; [RZUserDefaults() setObject:nil forKey:@"User List Mode Badge Colors -> +q"]; [RZUserDefaults() setObject:nil forKey:@"User List Mode Badge Colors -> +a"]; [RZUserDefaults() setObject:nil forKey:@"User List Mode Badge Colors -> +o"]; [RZUserDefaults() setObject:nil forKey:@"User List Mode Badge Colors -> +h"]; [RZUserDefaults() setObject:nil forKey:@"User List Mode Badge Colors -> +v"]; [RZUserDefaults() setObject:nil forKey:@"User List Mode Badge Colors -> no mode"]; [self onChangedUserListModeColor:nil]; } - (void)onResetServerListUnreadBadgeColorsToDefault:(id)sender { [self willChangeValueForKey:@"serverListUnreadCountBadgeHighlightColor"]; [RZUserDefaults() setObject:nil forKey:@"Server List Unread Message Count Badge Colors -> Highlight"]; [self didChangeValueForKey:@"serverListUnreadCountBadgeHighlightColor"]; [self onChangedServerListUnreadBadgeColor:sender]; } - (void)onChangedInputHistoryScheme:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionInputHistoryScope]; } - (void)onChangedAppearance:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionAppearance]; } - (void)onChangedTheme:(id)sender { [TPCPreferences performReloadAction:(TPCPreferencesReloadActionStyle | TPCPreferencesReloadActionTextDirection)]; } - (void)onThemeWillReload:(NSNotification *)notification { self.reloadingTheme = YES; } - (void)onThemeReloadComplete:(NSNotification *)notification { self.reloadingTheme = NO; if (self.reloadingThemeBySelection) { self.reloadingThemeBySelection = NO; [self onChangedThemeSelectionReloadComplete:notification]; } } - (void)onChangedChannelViewArrangement:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionChannelViewArrangement]; } - (void)onChangedMainWindowSegmentedController:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionTextFieldSegmentedControllerOrigin]; } - (void)onChangedUserListModeColor:(id)sender { static NSDictionary *preferenceMap = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ preferenceMap = @{ @(10) : @"User List Mode Badge Colors -> +y", @(9) : @"User List Mode Badge Colors -> +q", @(8) : @"User List Mode Badge Colors -> +a", @(7) : @"User List Mode Badge Colors -> +o", @(6) : @"User List Mode Badge Colors -> +h", @(5) : @"User List Mode Badge Colors -> +v", @(4) : @"User List Mode Badge Colors -> no mode" }; }); NSString *preferenceKey = preferenceMap[@([sender tag])]; /* -onResetUserListModeColorsToDefaults: passes nil sender */ if (preferenceKey == nil) { [TPCPreferences performReloadAction:(TPCPreferencesReloadActionMemberListUserBadges | TPCPreferencesReloadActionMemberList)]; } else { [TPCPreferences performReloadAction:TPCPreferencesReloadActionMemberListUserBadges forKey:preferenceKey]; } } - (void)onChangedMainInputTextViewFontSize:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionTextFieldFontSize]; } - (void)onFileTransferIPAddressDetectionMethodChanged:(id)sender { TXFileTransferIPAddressMethodDetection detectionMethod = [TPCPreferences fileTransferIPAddressDetectionMethod]; self.fileTransferManuallyEnteredIPAddressTextField.enabled = (detectionMethod == TXFileTransferIPAddressMethodManual); } - (void)onChangedHighlightLogging:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionHighlightLogging]; } - (void)onChangedUserListModeSortOrder:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionMemberListSortOrder]; } - (void)onChangedServerListUnreadBadgeColor:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionServerListUnreadBadges]; } - (void)onChangedScrollbackSaveLimit:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionScrollbackSaveLimit]; } - (void)onChangedScrollbackVisibleLimit:(id)sender { [TPCPreferences performReloadAction:TPCPreferencesReloadActionScrollbackVisibleLimit]; } - (void)onOpenPathToScripts:(id)sender { [RZWorkspace() openURL:[TPCPathInfo groupContainerApplicationSupportURL]]; } - (void)openPathToThemesCallback:(TDCAlertResponse)returnCode withOriginalAlert:(NSAlert *)originalAlert { NSParameterAssert(originalAlert != nil); if (returnCode == TDCAlertResponseDefault) { [self openPathToTheme]; } if (returnCode == TDCAlertResponseAlternate) { [self onModifyUserStyleSheetRules:nil]; } if (returnCode == TDCAlertResponseOther) { [originalAlert.window orderOut:nil]; [themeController() copyActiveThemeToDestinationLocation:TPCThemeStorageLocationCustom reloadOnCopy:YES openOnCopy:YES]; } } - (void)onOpenPathToTheme:(id)sender { if (themeController().bundledTheme) { [TDCAlert alertSheetWithWindow:NSApp.keyWindow body:TXTLS(@"TDCPreferencesController[ojj-ap]") title:TXTLS(@"TDCPreferencesController[5jv-aw]") defaultButton:TXTLS(@"TDCPreferencesController[6ws-av]") alternateButton:TXTLS(@"TDCPreferencesController[aib-iy]") otherButton:TXTLS(@"TDCPreferencesController[dj8-1t]") completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { [self openPathToThemesCallback:buttonClicked withOriginalAlert:underlyingAlert]; }]; return; } [self openPathToTheme]; } - (void)openPathToTheme { NSURL *fileURL = themeController().originalURL; [RZWorkspace() openURL:fileURL]; } - (void)onThemeListDidChange:(NSNotification *)aNote { [self updateThemeSelection]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [RZNotificationCenter() removeObserver:self]; [self saveWindowFrame]; if ([self.delegate respondsToSelector:@selector(preferencesDialogWillClose:)]) { [self.delegate preferencesDialogWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Preferences/TDCPreferencesNotificationConfiguration.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferencesLocalPrivate.h" #import "TDCPreferencesNotificationConfigurationPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TDCPreferencesNotificationConfiguration + (TDCPreferencesNotificationConfiguration *)objectWithEventType:(TXNotificationType)eventType { return [[self alloc] initWithEventType:eventType]; } - (nullable NSString *)alertSound { NSString *sound = [TPCPreferences soundForEvent:self.eventType]; if (sound == nil) { return TXNoAlertSoundPreferenceValue; } return sound; } - (void)setAlertSound:(nullable NSString *)alertSound { [TPCPreferences setSound:alertSound forEvent:self.eventType]; } - (NSUInteger)pushNotification { return [TPCPreferences notificationEnabledForEvent:self.eventType]; } - (void)setPushNotification:(NSUInteger)pushNotification { [TPCPreferences setNotificationEnabled:pushNotification forEvent:self.eventType]; } - (NSUInteger)speakEvent { return [TPCPreferences speakEvent:self.eventType]; } - (void)setSpeakEvent:(NSUInteger)speakEvent { [TPCPreferences setEventIsSpoken:speakEvent forEvent:self.eventType]; } - (NSUInteger)disabledWhileAway { return [TPCPreferences disabledWhileAwayForEvent:self.eventType]; } - (void)setDisabledWhileAway:(NSUInteger)disabledWhileAway { [TPCPreferences setDisabledWhileAway:disabledWhileAway forEvent:self.eventType]; } - (NSUInteger)bounceDockIcon { return [TPCPreferences bounceDockIconForEvent:self.eventType]; } - (void)setBounceDockIcon:(NSUInteger)bounceDockIcon { [TPCPreferences setBounceDockIcon:bounceDockIcon forEvent:self.eventType]; } - (NSUInteger)bounceDockIconRepeatedly { return [TPCPreferences bounceDockIconRepeatedlyForEvent:self.eventType]; } - (void)setBounceDockIconRepeatedly:(NSUInteger)bounceDockIconRepeatedly { [TPCPreferences setBounceDockIconRepeatedly:bounceDockIconRepeatedly forEvent:self.eventType]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Preferences/TDCPreferencesUserStyleSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOLocalization.h" #import "TPCPreferencesLocalPrivate.h" #import "TDCPreferencesUserStyleSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCPreferencesUserStyleSheet () @property (nonatomic, unsafe_unretained) IBOutlet NSTextView *rulesTextView; @property (nonatomic, assign) BOOL rulesChanged; @property (nonatomic, copy, readonly) NSString *defaultRules; @end @implementation TDCPreferencesUserStyleSheet - (instancetype)initWithWindow:(nullable NSWindow *)window { if ((self = [super initWithWindow:window])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCPreferencesUserStyleSheet" owner:self topLevelObjects:nil]; self.rulesTextView.font = [NSFont systemFontOfSize:13.0]; self.rulesTextView.textContainerInset = NSMakeSize(1, 3); [self loadRules]; } - (void)start { [self startSheet]; } - (void)textDidChange:(NSNotification *)aNotification { if (aNotification.object == self.rulesTextView) { self.rulesChanged = YES; } } - (void)loadRules { NSString *rules = [TPCPreferences themeUserStyleSheetRules]; /* We can define the default rules in the defaults property list but we don't do that for this preference so that it doesn't append to the HTML unless the user entered a value. It is easier to do that here versus making a comparison to the default in higher up. */ if (rules == nil) { rules = self.defaultRules; } self.rulesTextView.string = rules; self.rulesChanged = NO; } - (void)saveRules { NSString *rules = self.rulesTextView.string.trim; if (rules.length == 0 || [rules isEqualToString:self.defaultRules]) { rules = nil; } [TPCPreferences setThemeUserStyleSheetRules:rules]; if ([self.delegate respondsToSelector:@selector(userStyleSheetRulesChanged:)]) { [self.delegate userStyleSheetRulesChanged:self]; } } - (void)ok:(id)sender { if (self.rulesChanged) { [self saveRules]; } [super ok:nil]; } - (NSString *)defaultRules { return TXTLS(@"TDCPreferencesUserStyleSheet[q4s-3m]"); } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { if ([self.delegate respondsToSelector:@selector(userStyleSheetWillClose:)]) { [self.delegate userStyleSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Server Endpoint/TDCServerEndpointListSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCServer.h" #import "TVCBasicTableView.h" #import "TDCServerEndpointListSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _endpointEntryTableDragToken @"TDCServerEndpointListSheetEntryTableDragToken" @interface TDCServerEndpointListSheet () @property (nonatomic, strong) IBOutlet NSArrayController *entryTableController; @property (nonatomic, weak) IBOutlet TVCBasicTableView *entryTable; @property (nonatomic, weak) IBOutlet NSSegmentedControl *entryActionsSegmentedControl; - (IBAction)entryActionsSegmentedControlClicked:(id)sender; @end @implementation TDCServerEndpointListSheet - (instancetype)initWithWindow:(nullable NSWindow *)window { if ((self = [super initWithWindow:window])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCServerEndpointListSheet" owner:self topLevelObjects:nil]; [self.entryTable registerForDraggedTypes:@[_endpointEntryTableDragToken]]; [self.entryTableController addObserver:self forKeyPath:@"canRemove" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:NULL]; } - (void)startWithServerList:(NSArray *)serverList { NSParameterAssert(serverList != nil); for (IRCServer *server in serverList) { [self.entryTableController addObject:[server mutableCopy]]; } [self startSheet]; } - (void)ok:(id)sender { NSArray *serverListIn = self.entryTableController.arrangedObjects; NSMutableArray *serverListOut = [[NSMutableArray alloc] initWithCapacity:serverListIn.count]; for (IRCServerMutable *server in serverListIn) { /* New entries that are blank do not perform validation because nothing technically has changed. Instead of doing some complex workaround, let's just ditch objects with an empty server address. */ if (server.serverAddress.length == 0) { continue; } [serverListOut addObject:[server copy]]; } if ([self.delegate respondsToSelector:@selector(serverEndpointListSheet:onOk:)]) { [self.delegate serverEndpointListSheet:self onOk:[serverListOut copy]]; } [super ok:sender]; } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:@"canRemove"]) { [self updateEntryActionsSegmentedControlEnabledState]; } } #pragma mark - #pragma mark Entry Management - (void)updateEntryActionsSegmentedControlEnabledState { [self.entryActionsSegmentedControl setEnabled:self.entryTableController.canRemove forSegment:1]; } - (void)entryActionsSegmentedControlClicked:(id)sender { NSInteger selectedSegment = [sender selectedSegment]; if (selectedSegment == 0) { [self addEntry]; } else if (selectedSegment == 1) { [self removeSelectedEntry]; } } - (void)addEntry { IRCServerMutable *newEntry = [IRCServerMutable new]; [self.entryTableController addObject:newEntry]; /* Edit column next pass on the main thread to allow the -addObject to register properly. */ XRPerformBlockAsynchronouslyOnMainQueue(^{ NSTableView *tableView = self.entryTable; NSInteger rowSelection = (tableView.numberOfRows - 1); [tableView scrollRowToVisible:rowSelection]; [tableView editColumn:0 row:rowSelection withEvent:nil select:YES]; }); } - (void)removeSelectedEntry { NSIndexSet *selectedRows = self.entryTable.selectedRowIndexes; [self.entryTableController removeObjectsAtArrangedObjectIndexes:selectedRows]; } #pragma mark - #pragma mark Table View Delegate - (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pasteboard { NSData *draggedData = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes]; [pasteboard declareTypes:@[_endpointEntryTableDragToken] owner:self]; [pasteboard setData:draggedData forType:_endpointEntryTableDragToken]; return YES; } - (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation { return NSDragOperationGeneric; } - (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id )info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation { NSPasteboard *pasteboard = [info draggingPasteboard]; NSData *draggedData = [pasteboard dataForType:_endpointEntryTableDragToken]; NSIndexSet *draggedRowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:draggedData]; NSUInteger draggedRowIndex = draggedRowIndexes.firstIndex; [self.entryTableController moveObjectAtArrangedObjectIndex:draggedRowIndex toIndex:row]; return YES; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self.entryTableController removeObserver:self forKeyPath:@"canRemove"]; if ([self.delegate respondsToSelector:@selector(serverEndpointListSheetWillClose:)]) { [self.delegate serverEndpointListSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/Server Endpoint/TDCServerEndpointListSheetTable.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCServer.h" #import "NSStringHelper.h" #import "TLOLocalization.h" #import "TDCServerEndpointListSheetTablePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCServerEndpointListSheetTableCellView () @property (nonatomic, copy) NSString *serverAddress; @property (nonatomic, copy) NSString *serverPort; @property (nonatomic, copy) NSNumber *prefersSecuredConnection; @property (nonatomic, copy) NSString *serverPassword; @property (nonatomic, assign) BOOL observersRegistered; @end @implementation TDCServerEndpointListSheetTableCellView - (BOOL)validateValue:(inout id *)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError { if ([inKeyPath isEqualToString:@"serverAddress"]) { if (((NSString *)*ioValue).isValidInternetAddress == NO) { if (outError) { *outError = [NSError errorWithDomain:TXErrorDomain code:71013 userInfo:@{ NSLocalizedDescriptionKey : TXTLS(@"TDCServerEndpointListSheet[iis-gr]"), NSLocalizedRecoverySuggestionErrorKey : TXTLS(@"TDCServerEndpointListSheet[k0c-3u]")} ]; } return NO; } } else if ([inKeyPath isEqualToString:@"serverPort"]) { if (((NSString *)*ioValue).isValidInternetPort == NO) { if (outError) { *outError = [NSError errorWithDomain:TXErrorDomain code:71014 userInfo:@{ NSLocalizedDescriptionKey : TXTLS(@"TDCServerEndpointListSheet[qeb-ip]"), NSLocalizedRecoverySuggestionErrorKey : TXTLS(@"TDCServerEndpointListSheet[ox2-od]")} ]; } return NO; } } return YES; } - (NSString *)serverAddress { IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return @""; } return objectValue.serverAddress; } - (void)setServerAddress:(NSString *)serverAddress { IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return; } objectValue.serverAddress = serverAddress; } - (NSString *)serverPort { IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return @""; } return [NSString stringWithUnsignedShort:objectValue.serverPort]; } - (void)setServerPort:(NSString *)serverPort { IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return; } objectValue.serverPort = (uint16_t)serverPort.integerValue; } - (NSNumber *)prefersSecuredConnection { IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return @(NSControlStateValueOff); } if (objectValue.prefersSecuredConnection) { return @(NSControlStateValueOn); } else { return @(NSControlStateValueOff); } } - (void)setPrefersSecuredConnection:(NSNumber *)prefersSecuredConnection { IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return; } BOOL l_prefersSecuredConnection = (prefersSecuredConnection.unsignedIntegerValue == NSControlStateValueOn); if (l_prefersSecuredConnection) { objectValue.prefersSecuredConnection = YES; if (objectValue.serverPort == 6667) { objectValue.serverPort = 6697; } } else { objectValue.prefersSecuredConnection = NO; if (objectValue.serverPort == 6697) { objectValue.serverPort = 6667; } } } - (NSString *)serverPassword { IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return @""; } NSString *serverPassword = objectValue.serverPassword; if (serverPassword == nil) { return @""; } return serverPassword; } - (void)setServerPassword:(NSString *)serverPassword { IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return; } objectValue.serverPassword = serverPassword; } - (void)setObjectValue:(nullable id)objectValue { [self stopObservingObjectValue]; super.objectValue = objectValue; [self startObservingObjectValue]; } - (void)startObservingObjectValue { NSString *keyPath = self.identifier; [self willChangeValueForKey:keyPath]; [self didChangeValueForKey:keyPath]; IRCServerMutable *objectValue = self.objectValue; if (objectValue == nil) { return; } [objectValue addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; self.observersRegistered = YES; } - (void)stopObservingObjectValue { if (self.observersRegistered == NO) { return; } NSString *keyPath = self.identifier; IRCServerMutable *objectValue = self.objectValue; [objectValue removeObserver:self forKeyPath:keyPath]; self.observersRegistered = NO; } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:@"serverPort"]) { [self willChangeValueForKey:@"serverPort"]; [self didChangeValueForKey:@"serverPort"]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCAboutDialog.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" #import "TXMenuController.h" #import "TLOLocalization.h" #import "TPCApplicationInfo.h" #import "TDCAboutDialogPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCAboutDialog () @property (nonatomic, weak) IBOutlet NSTextField *versionInfoTextField; - (IBAction)displayAcknowledgements:(id)sender; @end @implementation TDCAboutDialog - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCAboutDialog" owner:self topLevelObjects:nil]; NSString *bundleVersion = [TPCApplicationInfo applicationVersionShort]; self.versionInfoTextField.stringValue = TXTLS(@"TDCAboutDialog[zjd-al]", bundleVersion); } - (void)show { [self.window restoreWindowStateForClass:self.class]; [super show]; } - (void)displayAcknowledgements:(id)sender { [menuController() openAcknowledgements:sender]; } - (void)windowWillClose:(NSNotification *)note { [self.window saveWindowStateForClass:self.class]; if ([self.delegate respondsToSelector:@selector(aboutDialogWillClose:)]) { [self.delegate aboutDialogWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCAddressBookSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "TLOLocalization.h" #import "TVCValidatedTextField.h" #import "TDCAddressBookSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCAddressBookSheet () @property (nonatomic, strong) IRCAddressBookEntryMutable *config; @property (nonatomic, assign) IRCAddressBookEntryType entryType; @property (nonatomic, strong) IBOutlet NSButton *ignoreClientToClientProtocolCheck; @property (nonatomic, strong) IBOutlet NSButton *ignoreFileTransferRequestsCheck; @property (nonatomic, strong) IBOutlet NSButton *ignoreGeneralEventMessagesCheck; @property (nonatomic, strong) IBOutlet NSButton *ignoreInlineMediaCheck; @property (nonatomic, strong) IBOutlet NSButton *ignoreNoticeMessagesCheck; @property (nonatomic, strong) IBOutlet NSButton *ignorePrivateMessageHighlightsCheck; @property (nonatomic, strong) IBOutlet NSButton *ignorePrivateMessagesCheck; @property (nonatomic, strong) IBOutlet NSButton *ignorePublicMessageHighlightsCheck; @property (nonatomic, strong) IBOutlet NSButton *ignorePublicMessagesCheck; @property (nonatomic, strong) IBOutlet NSButton *trackUserActivityCheck; @property (nonatomic, strong) IBOutlet NSButton *ignoreEntrySaveButton; @property (nonatomic, strong) IBOutlet NSButton *userTrackingEntrySaveButton; @property (nonatomic, strong) IBOutlet TVCValidatedTextField *ignoreEntryHostmaskTextField; @property (nonatomic, strong) IBOutlet TVCValidatedTextField *userTrackingEntryNicknameTextField; @property (nonatomic, strong) IBOutlet NSWindow *ignoreEntryView; @property (nonatomic, strong) IBOutlet NSWindow *userTrackingEntryView; @end @implementation TDCAddressBookSheet - (instancetype)initWithEntryType:(IRCAddressBookEntryType)entryType { NSParameterAssert(entryType == IRCAddressBookEntryTypeIgnore || entryType == IRCAddressBookEntryTypeUserTracking); if ((self = [super initWithWindow:nil])) { if (entryType == IRCAddressBookEntryTypeIgnore) { self.config = [IRCAddressBookEntryMutable newIgnoreEntry]; } else if (entryType == IRCAddressBookEntryTypeUserTracking) { self.config = [IRCAddressBookEntryMutable newUserTrackingEntry]; } self.entryType = entryType; [self prepareInitialState]; [self loadConfig]; return self; } return nil; } - (instancetype)initWithConfig:(IRCAddressBookEntry *)config { NSParameterAssert(config != nil); NSParameterAssert(config.entryType == IRCAddressBookEntryTypeIgnore || config.entryType == IRCAddressBookEntryTypeUserTracking); if ((self = [super initWithWindow:nil])) { self.config = [config mutableCopy]; self.entryType = config.entryType; [self prepareInitialState]; [self loadConfig]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCAddressBookSheet" owner:self topLevelObjects:nil]; self.ignoreEntryHostmaskTextField.stringValueIsInvalidOnEmpty = YES; self.ignoreEntryHostmaskTextField.stringValueUsesOnlyFirstToken = YES; self.ignoreEntryHostmaskTextField.validationBlock = ^NSString *(NSString *currentValue) { NSString *valueWithoutWildcard = [currentValue stringByReplacingOccurrencesOfString:@"*" withString:@"-"]; if (valueWithoutWildcard.isHostmask == NO) { return TXTLS(@"TDCAddressBookSheet[csu-bv]"); } return nil; }; self.userTrackingEntryNicknameTextField.stringValueIsInvalidOnEmpty = YES; self.userTrackingEntryNicknameTextField.stringValueUsesOnlyFirstToken = YES; self.userTrackingEntryNicknameTextField.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isHostmaskNickname == NO) { return TXTLS(@"CommonErrors[och-j5]"); } return nil; }; } - (void)loadConfig { if (self.entryType == IRCAddressBookEntryTypeIgnore) { self.ignoreEntryHostmaskTextField.stringValue = self.config.hostmask; self.ignoreClientToClientProtocolCheck.state = self.config.ignoreClientToClientProtocol; self.ignoreFileTransferRequestsCheck.state = self.config.ignoreFileTransferRequests; self.ignoreGeneralEventMessagesCheck.state = self.config.ignoreGeneralEventMessages; self.ignoreInlineMediaCheck.state = self.config.ignoreInlineMedia; self.ignoreNoticeMessagesCheck.state = self.config.ignoreNoticeMessages; self.ignorePrivateMessageHighlightsCheck.state = self.config.ignorePrivateMessageHighlights; self.ignorePrivateMessagesCheck.state = self.config.ignorePrivateMessages; self.ignorePublicMessageHighlightsCheck.state = self.config.ignorePublicMessageHighlights; self.ignorePublicMessagesCheck.state = self.config.ignorePublicMessages; } else if (self.entryType == IRCAddressBookEntryTypeUserTracking) { self.userTrackingEntryNicknameTextField.stringValue = self.config.hostmask; self.trackUserActivityCheck.state = self.config.trackUserActivity; } } - (void)start { if (self.entryType == IRCAddressBookEntryTypeIgnore) { self.sheet = self.ignoreEntryView; [self.sheet makeFirstResponder:self.ignoreEntryHostmaskTextField]; } else if (self.entryType == IRCAddressBookEntryTypeUserTracking) { self.sheet = self.userTrackingEntryView; [self.sheet makeFirstResponder:self.userTrackingEntryNicknameTextField]; } [self startSheet]; } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } if (self.entryType == IRCAddressBookEntryTypeIgnore) { self.config.hostmask = self.ignoreEntryHostmaskTextField.value; self.config.ignoreClientToClientProtocol = (self.ignoreClientToClientProtocolCheck.state == NSControlStateValueOn); self.config.ignoreFileTransferRequests = (self.ignoreFileTransferRequestsCheck.state == NSControlStateValueOn); self.config.ignoreGeneralEventMessages = (self.ignoreGeneralEventMessagesCheck.state == NSControlStateValueOn); self.config.ignoreInlineMedia = (self.ignoreInlineMediaCheck.state == NSControlStateValueOn); self.config.ignoreNoticeMessages = (self.ignoreNoticeMessagesCheck.state == NSControlStateValueOn); self.config.ignorePrivateMessageHighlights = (self.ignorePrivateMessageHighlightsCheck.state == NSControlStateValueOn); self.config.ignorePrivateMessages = (self.ignorePrivateMessagesCheck.state == NSControlStateValueOn); self.config.ignorePublicMessageHighlights = (self.ignorePublicMessageHighlightsCheck.state == NSControlStateValueOn); self.config.ignorePublicMessages = (self.ignorePublicMessagesCheck.state == NSControlStateValueOn); } else if (self.entryType == IRCAddressBookEntryTypeUserTracking) { self.config.hostmask = self.userTrackingEntryNicknameTextField.value; self.config.trackUserActivity = (self.trackUserActivityCheck.state == NSControlStateValueOn); } if ([self.delegate respondsToSelector:@selector(addressBookSheet:onOk:)]) { [self.delegate addressBookSheet:self onOk:[self.config copy]]; } [super ok:nil]; } - (BOOL)okOrError { if (self.entryType == IRCAddressBookEntryTypeIgnore) { return [self okOrErrorForTextField:self.ignoreEntryHostmaskTextField]; } else if (self.entryType == IRCAddressBookEntryTypeUserTracking) { return [self okOrErrorForTextField:self.userTrackingEntryNicknameTextField]; } return NO; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { if ([self.delegate respondsToSelector:@selector(addressBookSheetWillClose:)]) { [self.delegate addressBookSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCAlert.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOLocalization.h" #import "TPCPreferencesUserDefaults.h" #import "TDCAlert.h" NS_ASSUME_NONNULL_BEGIN NSString * const TDCAlertSuppressionPrefix = @"Text Input Prompt Suppression -> "; @interface TDCAlertContext : NSObject @property (nonatomic, copy, nullable) NSString *suppressionKey; @property (nonatomic, copy, nullable) TDCAlertCompletionBlock completionBlock; @end @implementation TDCAlert #pragma mark - #pragma mark Modal Alerts (Panel) + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate { return [self modalAlertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate suppressionKey:nil suppressionText:nil accessoryView:nil suppressionResponse:NULL]; } + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText { return [self modalAlertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate suppressionKey:suppressKey suppressionText:suppressText accessoryView:nil suppressionResponse:NULL]; } + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView { return [self modalAlertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate suppressionKey:suppressKey suppressionText:suppressText accessoryView:accessoryView suppressionResponse:NULL]; } + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText suppressionResponse:(nullable BOOL *)suppressionResponse { return [self modalAlertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate suppressionKey:suppressKey suppressionText:suppressText accessoryView:nil suppressionResponse:suppressionResponse]; } + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView suppressionResponse:(nullable BOOL *)suppressionResponse { TDCAlertResponse response = [self modalAlertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:nil suppressionKey:suppressKey suppressionText:suppressText accessoryView:nil suppressionResponse:nil]; return (response == TDCAlertResponseDefault); } + (TDCAlertResponse)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther { return [self modalAlertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:nil suppressionText:nil accessoryView:nil suppressionResponse:nil]; } + (TDCAlertResponse)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView suppressionResponse:(nullable BOOL *)suppressionResponse { NSParameterAssert(bodyText != nil); NSParameterAssert(titleText != nil); NSParameterAssert(buttonDefault != nil); /* Require main thread */ if ([NSThread isMainThread] == NO) { __block TDCAlertResponse result = TDCAlertResponseAlternate; XRPerformBlockSynchronouslyOnQueue(dispatch_get_main_queue(), ^{ result = [self modalAlertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:suppressKey suppressionText:suppressText accessoryView:accessoryView suppressionResponse:suppressionResponse]; }); return result; } /* Prepare suppression */ if (suppressKey) { suppressKey = [self suppressionKeyWithBase:suppressKey]; /* Exit if suppressed */ if ([RZUserDefaults() boolForKey:suppressKey]) { return TDCAlertResponseDefault; } } if (suppressKey && (suppressText == nil || suppressText.length == 0)) { suppressText = TXTLS(@"Prompts[68u-z9]"); } /* Construct alert */ NSAlert *alert = [NSAlert new]; alert.messageText = titleText; alert.informativeText = bodyText; [alert addButtonWithTitle:buttonDefault]; if (buttonAlternate) { [alert addButtonWithTitle:buttonAlternate]; } if (buttonOther) { [alert addButtonWithTitle:buttonOther]; } if (suppressKey || suppressText) { alert.showsSuppressionButton = YES; alert.suppressionButton.title = suppressText; } if (accessoryView) { alert.accessoryView = accessoryView; } /* Pop alert */ NSModalResponse returnCode = [alert runModal]; TDCAlertResponse response = [self _convertResponseFromNSAlert:returnCode]; [self _finalizeAlert:alert withResponse:response completionBlock:nil suppressionKey:suppressKey suppressionResponse:suppressionResponse]; return response; } #pragma mark - #pragma mark Non-blocking Alerts (Panel) + (TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate { /* Will never return nil because no suppression. */ return (TVCAlert * _Nonnull) [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:nil suppressionKey:nil suppressionText:nil accessoryView:nil completionBlock:nil]; } + (TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther { return (TVCAlert * _Nonnull) [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:nil suppressionText:nil accessoryView:nil completionBlock:nil]; } + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText { return [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:nil suppressionKey:suppressKey suppressionText:suppressText accessoryView:nil completionBlock:nil]; } + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { return [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:nil suppressionKey:nil suppressionText:nil accessoryView:nil completionBlock:completionBlock]; } + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { return [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:nil suppressionText:nil accessoryView:nil completionBlock:completionBlock]; } + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { return [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:nil suppressionKey:suppressKey suppressionText:suppressText accessoryView:nil completionBlock:completionBlock]; } + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { return [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:suppressKey suppressionText:suppressText accessoryView:nil completionBlock:completionBlock]; } + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { return [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:nil suppressionKey:suppressKey suppressionText:suppressText accessoryView:accessoryView completionBlock:completionBlock]; } + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { NSParameterAssert(bodyText != nil); NSParameterAssert(titleText != nil); NSParameterAssert(buttonDefault != nil); /* Require main thread */ if ([NSThread isMainThread] == NO) { __block TVCAlert *alert = nil; XRPerformBlockSynchronouslyOnQueue(dispatch_get_main_queue(), ^{ alert = [self alertWithMessage:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate suppressionKey:suppressKey suppressionText:suppressText accessoryView:accessoryView completionBlock:completionBlock]; }); return alert; } /* Prepare suppression */ if (suppressKey) { suppressKey = [self suppressionKeyWithBase:suppressKey]; /* Exit if suppressed */ if ([RZUserDefaults() boolForKey:suppressKey]) { if (completionBlock) { completionBlock(TDCAlertResponseDefault, YES, nil); } return nil; } } if (suppressKey && (suppressText == nil || suppressText.length == 0)) { suppressText = TXTLS(@"Prompts[68u-z9]"); } /* Construct alert */ TVCAlert *alert = [TVCAlert new]; alert.messageText = titleText; alert.informativeText = bodyText; [alert setTitle:buttonDefault forButton:TVCAlertResponseButtonFirst]; if (buttonAlternate) { [alert setTitle:buttonAlternate forButton:TVCAlertResponseButtonSecond]; } if (buttonOther) { [alert setTitle:buttonOther forButton:TVCAlertResponseButtonThird]; } if (suppressKey || suppressText) { alert.showsSuppressionButton = YES; alert.suppressionButton.title = suppressText; } if (accessoryView) { alert.accessoryView = accessoryView; } /* Pop alert */ [alert showAlertWithCompletionBlock:^(TVCAlert *sender, TVCAlertResponseButton buttonClicked) { [self _finalizeAlert:alert withResponse:[self _convertResponseFromTVCAlert:buttonClicked] completionBlock:completionBlock suppressionKey:suppressKey suppressionResponse:NULL]; }]; return alert; } #pragma mark - #pragma mark Non-blocking Alerts (Sheet) + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther { [self alertSheetWithWindow:window body:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:nil suppressionText:nil accessoryView:nil completionBlock:nil]; } + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { [self alertSheetWithWindow:window body:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:nil suppressionText:nil accessoryView:nil completionBlock:completionBlock]; } + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther accessoryView:(nullable NSView *)accessoryView completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { [self alertSheetWithWindow:window body:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:nil suppressionText:nil accessoryView:accessoryView completionBlock:completionBlock]; } + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { [self alertSheetWithWindow:window body:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:suppressKey suppressionText:suppressText accessoryView:nil completionBlock:completionBlock]; } + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView completionBlock:(nullable TDCAlertCompletionBlock)completionBlock { NSParameterAssert(window != nil); NSParameterAssert(bodyText != nil); NSParameterAssert(titleText != nil); NSParameterAssert(buttonDefault != nil); /* Require main thread */ if ([NSThread isMainThread] == NO) { XRPerformBlockSynchronouslyOnQueue(dispatch_get_main_queue(), ^{ [self alertSheetWithWindow:window body:bodyText title:titleText defaultButton:buttonDefault alternateButton:buttonAlternate otherButton:buttonOther suppressionKey:suppressKey suppressionText:suppressText accessoryView:accessoryView completionBlock:completionBlock]; }); return; } /* Prepare suppression */ if (suppressKey) { suppressKey = [self suppressionKeyWithBase:suppressKey]; /* Exit if suppressed */ if ([RZUserDefaults() boolForKey:suppressKey]) { if (completionBlock) { completionBlock(TDCAlertResponseDefault, YES, nil); } return; } } if (suppressKey && (suppressText == nil || suppressText.length == 0)) { suppressText = TXTLS(@"Prompts[68u-z9]"); } /* Construct alert */ NSAlert *alert = [NSAlert new]; alert.alertStyle = NSAlertStyleInformational; alert.messageText = titleText; alert.informativeText = bodyText; [alert addButtonWithTitle:buttonDefault]; if (buttonAlternate) { [alert addButtonWithTitle:buttonAlternate]; } if (buttonOther) { [alert addButtonWithTitle:buttonOther]; } if (suppressKey || suppressText) { alert.showsSuppressionButton = YES; alert.suppressionButton.title = suppressText; } if (accessoryView) { alert.accessoryView = accessoryView; } /* Construct alert context */ TDCAlertContext *context = [TDCAlertContext new]; context.suppressionKey = suppressKey; context.completionBlock = completionBlock; /* Pop alert */ [alert beginSheetModalForWindow:window completionHandler:^(NSModalResponse returnCode) { [self _alertSheetResponseCallback:alert returnCode:returnCode contextInfo:context]; }]; } + (void)_alertSheetResponseCallback:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(TDCAlertContext *)context { NSString *suppressionKey = context.suppressionKey; TDCAlertCompletionBlock completionBlock = context.completionBlock; [self _finalizeAlert:alert withResponse:[self _convertResponseFromNSAlert:returnCode] completionBlock:completionBlock suppressionKey:suppressionKey suppressionResponse:NULL]; } #pragma mark - #pragma mark Utilities + (NSString *)suppressionKeyWithBase:(NSString *)base { NSParameterAssert(base != nil); if ([base hasPrefix:TDCAlertSuppressionPrefix]) { return base; } return [TDCAlertSuppressionPrefix stringByAppendingString:base]; } + (void)_finalizeAlert:(nullable id)underlyingAlert withResponse:(TDCAlertResponse)response completionBlock:(nullable TDCAlertCompletionBlock)completionBlock suppressionKey:(nullable NSString *)suppressionKey suppressionResponse:(nullable BOOL *)suppressionResponse { BOOL suppressed = [self _recordSuppressionForButton:[underlyingAlert suppressionButton] withKey:suppressionKey]; if ( suppressionResponse) { *suppressionResponse = suppressed; } if (completionBlock) { completionBlock(response, suppressed, underlyingAlert); } } + (BOOL)_recordSuppressionForButton:(nullable NSButton *)suppressionButton withKey:(nullable NSString *)suppressionKey { /* There are some times when we use suppression text field to give user option that doesn't mean to suppress the prompt next time. We therefore only return NO for both arguments missing and not just one. */ if (suppressionButton == nil && suppressionKey == nil) { return NO; } BOOL suppressed = (suppressionButton.state == NSControlStateValueOn); if (suppressed && suppressionKey != nil) { [RZUserDefaults() setBool:YES forKey:suppressionKey]; } return suppressed; } + (TDCAlertResponse)_convertResponseFromNSAlert:(NSUInteger)response { switch (response) { case NSAlertSecondButtonReturn: { return TDCAlertResponseAlternate; } case NSAlertThirdButtonReturn: { return TDCAlertResponseOther; } default: { return TDCAlertResponseDefault; } } } + (TDCAlertResponse)_convertResponseFromTVCAlert:(TVCAlertResponseButton)response { switch (response) { case TVCAlertResponseButtonSecond: { return TDCAlertResponseAlternate; } case TVCAlertResponseButtonThird: { return TDCAlertResponseOther; } default: { return TDCAlertResponseDefault; } } } @end #pragma mark - @implementation TDCAlertContext @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCChannelBanListSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXGlobalModels.h" #import "TLOLocalization.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCISupportInfo.h" #import "TVCBasicTableView.h" #import "TDCChannelBanListSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelBanListSheetEntry : NSObject @property (nonatomic, copy) NSString *entryMask; @property (nonatomic, copy) NSString *entryAuthor; @property (readonly, copy) NSString *entryCreationDateString; @property (nonatomic, copy, nullable) NSDate *entryCreationDate; @end @interface TDCChannelBanListSheet () @property (nonatomic, strong, readwrite) IRCClient *client; @property (nonatomic, strong, readwrite) IRCChannel *channel; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, copy, readwrite) NSString *channelId; @property (nonatomic, assign, readwrite) TDCChannelBanListSheetEntryType entryType; @property (nonatomic, copy, readwrite, nullable) NSArray *listOfChanges; @property (nonatomic, weak) IBOutlet NSTextField *headerTitleTextField; @property (nonatomic, weak) IBOutlet TVCBasicTableView *entryTable; @property (nonatomic, strong) IBOutlet NSArrayController *entryTableController; - (IBAction)onUpdate:(id)sender; - (IBAction)onRemoveEntry:(id)sender; @end @implementation TDCChannelBanListSheet - (nullable instancetype)initWithEntryType:(TDCChannelBanListSheetEntryType)entryType inChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ([self.class channel:channel supportsEntryType:entryType] == NO) { return nil; } if ((self = [super initWithWindow:nil])) { self.entryType = entryType; self.client = channel.associatedClient; self.clientId = channel.associatedClient.uniqueIdentifier; self.channel = channel; self.channelId = channel.uniqueIdentifier; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCChannelBanListSheet" owner:self topLevelObjects:nil]; self.entryTable.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"entryCreationDate" ascending:NO selector:@selector(compare:)] ]; NSString *headerTitle = nil; if (self.entryType == TDCChannelBanListSheetEntryTypeBan) { headerTitle = TXTLS(@"TDCChannelBanListSheet[rhc-ke]", self.channel.name); } else if (self.entryType == TDCChannelBanListSheetEntryTypeBanException) { headerTitle = TXTLS(@"TDCChannelBanListSheet[gbi-wn]", self.channel.name); } else if (self.entryType == TDCChannelBanListSheetEntryTypeInviteException) { headerTitle = TXTLS(@"TDCChannelBanListSheet[ylc-6e]", self.channel.name); } else if (self.entryType == TDCChannelBanListSheetEntryTypeQuiet) { headerTitle = TXTLS(@"TDCChannelBanListSheet[g4r-t6]", self.channel.name); } self.headerTitleTextField.stringValue = headerTitle; } - (void)start { [self startSheet]; } - (void)clear { self.entryTableController.content = nil; } - (void)addEntry:(NSString *)entryMask setBy:(nullable NSString *)entryAuthor creationDate:(nullable NSDate *)entryCreationDate { NSParameterAssert(entryMask != nil); if (entryAuthor == nil) { entryAuthor = TXTLS(@"BasicLanguage[vbl-xi]"); // "Unknown" } TDCChannelBanListSheetEntry *newEntry = [TDCChannelBanListSheetEntry new]; newEntry.entryMask = entryMask; newEntry.entryAuthor = entryAuthor; newEntry.entryCreationDate = entryCreationDate; [self willChangeValueForKey:@"entryCount"]; [self.entryTableController addObject:newEntry]; [self didChangeValueForKey:@"entryCount"]; } - (NSNumber *)entryCount { return @([self.entryTableController.arrangedObjects count]); } #pragma mark - #pragma mark Actions - (void)onUpdate:(id)sender { [self clear]; if ([self.delegate respondsToSelector:@selector(channelBanListSheetOnUpdate:)]) { [self.delegate channelBanListSheetOnUpdate:self]; } } - (void)onRemoveEntry:(id)sender { NSIndexSet *selectedRows = self.entryTable.selectedRowIndexes; NSMutableArray *selectedEntries = [NSMutableArray arrayWithCapacity:selectedRows.count]; [selectedRows enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { TDCChannelBanListSheetEntry *entryItem = self.entryTableController.arrangedObjects[index]; [selectedEntries addObject:entryItem.entryMask]; }]; self.listOfChanges = [self.client compileListOfModeChangesForModeSymbol:self.modeSymbol modeIsSet:NO modeParameters:selectedEntries]; [super cancel:nil]; } #pragma mark - #pragma mark Utilities + (BOOL)channel:(IRCChannel *)channel supportsEntryType:(TDCChannelBanListSheetEntryType)entryType { return [channel.associatedClient.supportInfo isListSupported:(IRCISupportInfoListType)entryType]; } - (NSString *)modeSymbol { /* -modeSymbolForList: is nullable but because we only allow this class to be created if the mode is already supported, then we can advertise it here as non-nil */ return [self.client.supportInfo modeSymbolForList:(IRCISupportInfoListType)self.entryType]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { if ([self.delegate respondsToSelector:@selector(channelBanListSheetWillClose:)]) { [self.delegate channelBanListSheetWillClose:self]; } } @end #pragma mark - @implementation TDCChannelBanListSheetEntry - (NSString *)entryCreationDateString { NSDate *entryCreationDate = self.entryCreationDate; if (entryCreationDate == nil) { return TXTLS(@"BasicLanguage[vbl-xi]"); // "Unknown" } return TXFormatDateLongStyle(entryCreationDate, YES); } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCChannelInviteSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClient.h" #import "TLOLocalization.h" #import "TDCChannelInviteSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelInviteSheet () @property (nonatomic, strong, readwrite) IRCClient *client; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, copy, readwrite) NSArray *nicknames; @property (nonatomic, weak) IBOutlet NSTextField *headerTitleTextField; @property (nonatomic, weak) IBOutlet NSPopUpButton *channelListPopup; @end @implementation TDCChannelInviteSheet - (instancetype)initWithNicknames:(NSArray *)nicknames onClient:(IRCClient *)client { NSParameterAssert(nicknames != nil); NSParameterAssert(client != nil); if ((self = [super initWithWindow:nil])) { self.nicknames = nicknames; self.client = client; self.clientId = client.uniqueIdentifier; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCChannelInviteSheet" owner:self topLevelObjects:nil]; NSUInteger nicknameCount = self.nicknames.count; NSString *headerTitle = nil; if (nicknameCount == 1) { headerTitle = self.nicknames[0]; } else if (nicknameCount == 2) { headerTitle = TXTLS(@"TDCChannelInviteSheet[7i1-ds]", self.nicknames[0], self.nicknames[1]); } else { headerTitle = TXTLS(@"TDCChannelInviteSheet[c8p-sb]", nicknameCount); } self.headerTitleTextField.stringValue = TXTLS(@"TDCChannelInviteSheet[0lg-er]", headerTitle); } - (void)startWithChannels:(NSArray *)channels { NSParameterAssert(channels != nil); for (NSString *channel in channels) { [self.channelListPopup addItemWithTitle:channel]; } [self startSheet]; } - (void)ok:(id)sender { if ([self.delegate respondsToSelector:@selector(channelInviteSheet:onSelectChannel:)]) { NSString *channelName = self.channelListPopup.titleOfSelectedItem; [self.delegate channelInviteSheet:self onSelectChannel:channelName]; } [super ok:nil]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { if ([self.delegate respondsToSelector:@selector(channelInviteSheetWillClose:)]) { [self.delegate channelInviteSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCChannelModifyModesSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TLOLocalization.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCChannelMode.h" #import "IRCISupportInfo.h" #import "IRCModeInfo.h" #import "TDCAlert.h" #import "TDCChannelModifyModesSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelModifyModesSheet () @property (nonatomic, strong, readwrite) IRCClient *client; @property (nonatomic, strong, readwrite) IRCChannel *channel; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, copy, readwrite) NSString *channelId; @property (nonatomic, strong) IRCChannelModeContainer *modes; @property (nonatomic, weak) IBOutlet NSButton *sCheck; @property (nonatomic, weak) IBOutlet NSButton *pCheck; @property (nonatomic, weak) IBOutlet NSButton *nCheck; @property (nonatomic, weak) IBOutlet NSButton *tCheck; @property (nonatomic, weak) IBOutlet NSButton *iCheck; @property (nonatomic, weak) IBOutlet NSButton *mCheck; @property (nonatomic, weak) IBOutlet NSButton *kCheck; @property (nonatomic, weak) IBOutlet NSButton *lCheck; @property (nonatomic, weak) IBOutlet NSTextField *kText; @property (nonatomic, weak) IBOutlet NSTextField *lText; @property (nonatomic, copy) NSString *channelUserLimitMode; @property (nonatomic, assign) BOOL secretKeyLengthAlertDisplayed; - (IBAction)onChangeCheck:(id)sender; @end @implementation TDCChannelModifyModesSheet - (instancetype)initWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ((self = [super initWithWindow:nil])) { self.client = channel.associatedClient; self.clientId = channel.associatedClient.uniqueIdentifier; self.channel = channel; self.channelId = channel.uniqueIdentifier; self.modes = [channel.modeInfo.modes copy]; [self prepareInitialState]; [self loadConfig]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCChannelModifyModesSheet" owner:self topLevelObjects:nil]; } - (void)loadConfig { self.iCheck.state = [self.modes modeInfoFor:@"i"].modeIsSet; self.mCheck.state = [self.modes modeInfoFor:@"m"].modeIsSet; self.nCheck.state = [self.modes modeInfoFor:@"n"].modeIsSet; self.pCheck.state = [self.modes modeInfoFor:@"p"].modeIsSet; self.sCheck.state = [self.modes modeInfoFor:@"s"].modeIsSet; self.tCheck.state = [self.modes modeInfoFor:@"t"].modeIsSet; IRCModeInfo *kModeInfo = [self.modes modeInfoFor:@"k"]; self.kCheck.state = kModeInfo.modeIsSet; if (kModeInfo.modeIsSet) { self.kText.stringValue = kModeInfo.modeParameter; } IRCModeInfo *lModeInfo = [self.modes modeInfoFor:@"l"]; self.lCheck.state = lModeInfo.modeIsSet; if (lModeInfo.modeIsSet) { self.channelUserLimitMode = lModeInfo.modeParameter; // Set to local property for validation } [self updateTextFields]; } - (void)start { [self startSheet]; } - (BOOL)validateValue:(inout id *)value forKey:(NSString *)key error:(out NSError **)outError { if ([key isEqualToString:@"channelUserLimitMode"]) { NSInteger valueInteger = [*value integerValue]; if (valueInteger < 0) { *value = [NSString stringWithInteger:0]; } else if (valueInteger > 99999) { *value = [NSString stringWithInteger:99999]; } } return YES; } - (void)updateTextFields { self.kText.enabled = (self.kCheck.state == NSControlStateValueOn); self.lText.enabled = (self.lCheck.state == NSControlStateValueOn); } - (void)onChangeCheck:(id)sender { [self updateTextFields]; if (self.sCheck.state == NSControlStateValueOn && self.pCheck.state == NSControlStateValueOn) { if (sender == self.sCheck) { self.pCheck.state = NSControlStateValueOff; } else { self.sCheck.state = NSControlStateValueOff; } } } - (void)controlTextDidChange:(NSNotification *)aNotification { if (aNotification.object == self.kText) { [self updateSecretKeyLengthAlert]; } } - (void)updateSecretKeyLengthAlert { NSUInteger maximumKeyLength = self.client.supportInfo.maximumKeyLength; if (maximumKeyLength == 0) { return; } NSUInteger currentKeyLength = self.kText.stringValue.length; if (currentKeyLength <= maximumKeyLength) { return; } if (self.secretKeyLengthAlertDisplayed == NO) { self.secretKeyLengthAlertDisplayed = YES; } else { return; } [TDCAlert alertSheetWithWindow:self.sheet body:TXTLS(@"TDCChannelModifyModesSheet[lir-ra]") title:TXTLS(@"TDCChannelModifyModesSheet[7m9-39]", self.client.networkNameAlt, maximumKeyLength) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil otherButton:nil suppressionKey:@"maximum_secret_key_length" suppressionText:nil completionBlock:nil]; } - (void)ok:(id)sender { [self.modes changeMode:@"i" modeIsSet:(self.iCheck.state == NSControlStateValueOn)]; [self.modes changeMode:@"m" modeIsSet:(self.mCheck.state == NSControlStateValueOn)]; [self.modes changeMode:@"n" modeIsSet:(self.nCheck.state == NSControlStateValueOn)]; [self.modes changeMode:@"p" modeIsSet:(self.pCheck.state == NSControlStateValueOn)]; [self.modes changeMode:@"s" modeIsSet:(self.sCheck.state == NSControlStateValueOn)]; [self.modes changeMode:@"t" modeIsSet:(self.tCheck.state == NSControlStateValueOn)]; [self.modes changeMode:@"k" modeIsSet:(self.kCheck.state == NSControlStateValueOn) modeParameter:self.kText.stringValue]; [self.modes changeMode:@"l" modeIsSet:(self.lCheck.state == NSControlStateValueOn) modeParameter:self.lText.stringValue]; if ([self.delegate respondsToSelector:@selector(channelModifyModesSheet:onOk:)]) { [self.delegate channelModifyModesSheet:self onOk:self.modes]; } [super ok:nil]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { if ([self.delegate respondsToSelector:@selector(channelModifyModesSheetWillClose:)]) { [self.delegate channelModifyModesSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCChannelModifyTopicSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TLOLocalization.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCISupportInfo.h" #import "TVCTextViewWithIRCFormatterPrivate.h" #import "TDCAlert.h" #import "TDCChannelModifyTopicSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelModifyTopicSheet () @property (nonatomic, strong, readwrite) IRCClient *client; @property (nonatomic, strong, readwrite) IRCChannel *channel; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, copy, readwrite) NSString *channelId; @property (nonatomic, weak) IBOutlet NSTextField *headerTitleTextField; @property (nonatomic, strong) IBOutlet TVCTextViewWithIRCFormatter *topicValueTextField; @property (nonatomic, assign) BOOL topicLengthAlertDisplayed; @end @implementation TDCChannelModifyTopicSheet - (instancetype)initWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ((self = [super initWithWindow:nil])) { self.client = channel.associatedClient; self.clientId = channel.associatedClient.uniqueIdentifier; self.channel = channel; self.channelId = channel.uniqueIdentifier; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCChannelModifyTopicSheet" owner:self topLevelObjects:nil]; NSString *headerTitle = [NSString stringWithFormat:self.headerTitleTextField.stringValue, self.channel.name]; self.headerTitleTextField.stringValue = headerTitle; self.topicValueTextField.preferredFont = [NSFont systemFontOfSize:13.0]; self.topicValueTextField.preferredFontColor = [NSColor textColor]; NSString *topic = self.channel.topic; if (topic) { self.topicValueTextField.stringValueWithIRCFormatting = topic; } } - (void)start { [self startSheet]; } - (void)textDidChange:(NSNotification *)aNotification { [self.topicValueTextField textDidChange:aNotification]; [self updateTopicLengthAlert]; } - (void)updateTopicLengthAlert { NSUInteger maximumTopicLength = self.client.supportInfo.maximumTopicLength; if (maximumTopicLength == 0) { return; } NSUInteger currentTopicLength = self.topicValueTextField.stringLength; if (currentTopicLength <= maximumTopicLength) { return; } if (self.topicLengthAlertDisplayed == NO) { self.topicLengthAlertDisplayed = YES; } else { return; } [TDCAlert alertSheetWithWindow:self.sheet body:TXTLS(@"TDCChannelModifyTopicSheet[zm4-cr]") title:TXTLS(@"TDCChannelModifyTopicSheet[27l-qx]", self.client.networkNameAlt, maximumTopicLength) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil otherButton:nil suppressionKey:@"maximum_topic_length" suppressionText:nil completionBlock:nil]; } - (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector { if (aSelector == @selector(insertNewline:)) { [self ok:nil]; return YES; } else if (aSelector == @selector(insertNewlineIgnoringFieldEditor:) ) { /* Do not allow a new line to be inserted using Option + Enter */ return YES; } return NO; } - (void)ok:(id)sender { if ([self.delegate respondsToSelector:@selector(channelModifyTopicSheet:onOk:)]) { NSString *formattedTopic = self.topicValueTextField.stringValueWithIRCFormatting; NSString *topicWithoutNewlines = [formattedTopic stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; [self.delegate channelModifyTopicSheet:self onOk:topicWithoutNewlines]; } [super ok:nil]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { if ([self.delegate respondsToSelector:@selector(channelModifyTopicSheetWillClose:)]) { [self.delegate channelModifyTopicSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCChannelPropertiesNotificationConfiguration.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelConfig.h" #import "TDCChannelPropertiesSheetInternal.h" #import "TDCChannelPropertiesNotificationConfigurationPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelPropertiesNotificationConfiguration () @property (nonatomic, weak) TDCChannelPropertiesSheet *sheet; @property (readonly) IRCChannelConfigMutable *config; @end @implementation TDCChannelPropertiesNotificationConfiguration - (instancetype)initWithEventType:(TXNotificationType)aEventType inSheet:(TDCChannelPropertiesSheet *)sheet { NSParameterAssert(sheet != nil); if ((self = [super initWithEventType:aEventType])) { self.sheet = sheet; return self; } return nil; } - (IRCChannelConfigMutable *)config { return self.sheet.config; } - (nullable NSString *)alertSound { return [self.config soundForEvent:self.eventType]; } - (void)setAlertSound:(nullable NSString *)alertSound { [self.config setSound:alertSound forEvent:self.eventType]; } - (NSUInteger)pushNotification { return [self.config notificationEnabledForEvent:self.eventType]; } - (void)setPushNotification:(NSUInteger)pushNotification { [self.config setNotificationEnabled:pushNotification forEvent:self.eventType]; } - (NSUInteger)speakEvent { return [self.config speakEvent:self.eventType]; } - (void)setSpeakEvent:(NSUInteger)speakEvent { [self.config setEventIsSpoken:speakEvent forEvent:self.eventType]; } - (NSUInteger)disabledWhileAway { return [self.config disabledWhileAwayForEvent:self.eventType]; } - (void)setDisabledWhileAway:(NSUInteger)disabledWhileAway { [self.config setDisabledWhileAway:disabledWhileAway forEvent:self.eventType]; } - (NSUInteger)bounceDockIcon { return [self.config bounceDockIconForEvent:self.eventType]; } - (void)setBounceDockIcon:(NSUInteger)bounceDockIcon { [self.config setBounceDockIcon:bounceDockIcon forEvent:self.eventType]; } - (NSUInteger)bounceDockIconRepeatedly { return [self.config bounceDockIconRepeatedlyForEvent:self.eventType]; } - (void)setBounceDockIconRepeatedly:(NSUInteger)bounceDockIconRepeatedly { [self.config setBounceDockIconRepeatedly:bounceDockIconRepeatedly forEvent:self.eventType]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCChannelPropertiesSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "NSViewHelper.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCISupportInfo.h" #import "TPCPreferencesLocal.h" #import "TLOLocalization.h" #import "TVCLogControllerInlineMediaServicePrivate.h" #import "TVCNotificationConfigurationViewControllerPrivate.h" #import "TVCValidatedTextField.h" #import "TDCAlert.h" #import "TDCChannelPropertiesNotificationConfigurationPrivate.h" #import "TDCChannelPropertiesSheetInternal.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TDCChannelPropertiesSheetSelection) { TDCChannelPropertiesSheetSelectionGeneral = 0, TDCChannelPropertiesSheetSelectionDefaults = 1, TDCChannelPropertiesSheetSelectionNotifications = 2 }; @interface TDCChannelPropertiesSheet () @property (nonatomic, strong, readwrite, nullable) IRCClient *client; @property (nonatomic, strong, readwrite, nullable) IRCChannel *channel; @property (nonatomic, copy, readwrite, nullable) NSString *clientId; @property (nonatomic, copy, readwrite, nullable) NSString *channelId; @property (nonatomic, assign) BOOL isNewConfiguration; @property (nonatomic, assign) BOOL secretKeyLengthAlertDisplayed; @property (nonatomic, copy) NSArray *navigationTree; @property (nonatomic, weak) IBOutlet NSButton *autoJoinCheck; @property (nonatomic, weak) IBOutlet NSButton *disableInlineMediaCheck; @property (nonatomic, weak) IBOutlet NSButton *enableInlineMediaCheck; @property (nonatomic, weak) IBOutlet NSButton *pushNotificationsCheck; @property (nonatomic, weak) IBOutlet NSButton *showTreeBadgeCountCheck; @property (nonatomic, weak) IBOutlet NSButton *ignoreHighlightsCheck; @property (nonatomic, weak) IBOutlet NSButton *ignoreGeneralEventMessagesCheck; @property (nonatomic, weak) IBOutlet NSSegmentedControl *contentViewTabView; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *channelNameTextField; @property (nonatomic, weak) IBOutlet NSTextField *labelTextField; @property (nonatomic, weak) IBOutlet NSTextField *defaultModesTextField; @property (nonatomic, weak) IBOutlet NSTextField *defaultTopicTextField; @property (nonatomic, weak) IBOutlet NSTextField *secretKeyTextField; @property (nonatomic, strong) IBOutlet NSView *contentView; @property (nonatomic, strong) IBOutlet NSView *contentViewDefaultsView; @property (nonatomic, strong) IBOutlet NSView *contentViewGeneralView; @property (nonatomic, strong) IBOutlet NSView *contentViewNotifications; @property (nonatomic, strong) IBOutlet NSView *contentViewNotificationsHost; @property (nonatomic, strong) IBOutlet TVCNotificationConfigurationViewController *notificationsController; - (IBAction)onMenuBarItemChanged:(id)sender; - (IBAction)onInlineMediaCheckChanged:(id)sender; - (IBAction)onPushNotificationsCheckChanged:(id)sender; @end @implementation TDCChannelPropertiesSheet DESIGNATED_INITIALIZER_EXCEPTION_BODY_BEGIN - (instancetype)initWithWindow:(nullable NSWindow *)window { if ((self = [super initWithWindow:window])) { self.config = [IRCChannelConfigMutable new]; [self prepareInitialState]; [self loadConfig]; return self; } return nil; } DESIGNATED_INITIALIZER_EXCEPTION_BODY_END - (instancetype)initWithClient:(IRCClient *)client { return [self initWithConfig:nil onClient:client]; } - (instancetype)initWithClientId:(NSString *)clientId { return [self initWithConfig:nil onClientWithId:clientId]; } - (instancetype)initWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ((self = [super initWithWindow:nil])) { self.client = channel.associatedClient; self.clientId = channel.associatedClient.uniqueIdentifier; self.channel = channel; self.channelId = channel.uniqueIdentifier; if (channel) { self.config = [channel.config mutableCopy]; } else { self.config = [IRCChannelConfigMutable new]; } [self prepareInitialState]; [self loadConfig]; return self; } return nil; } - (instancetype)initWithConfig:(nullable IRCChannelConfig *)config { return [self initWithConfig:config onClientWithId:nil]; } - (instancetype)initWithConfig:(nullable IRCChannelConfig *)config onClient:(nullable IRCClient *)client { if ((self = [super initWithWindow:nil])) { self.client = client; self.clientId = client.uniqueIdentifier; if (config) { self.config = [config mutableCopy]; } else { self.config = [IRCChannelConfigMutable new]; } [self prepareInitialState]; [self loadConfig]; return self; } return nil; } - (instancetype)initWithConfig:(nullable IRCChannelConfig *)config onClientWithId:(nullable NSString *)clientId { if ((self = [self initWithWindow:nil])) { self.clientId = clientId; if (config) { self.config = [config mutableCopy]; } else { self.config = [IRCChannelConfigMutable new]; } [self prepareInitialState]; [self loadConfig]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCChannelPropertiesSheet" owner:self topLevelObjects:nil]; self.navigationTree = @[ // view first responder @[self.contentViewGeneralView, self.channelNameTextField], @[self.contentViewDefaultsView, self.defaultTopicTextField], @[self.contentViewNotifications, [NSNull null]], ]; self.channelNameTextField.stringValueIsInvalidOnEmpty = YES; self.channelNameTextField.stringValueUsesOnlyFirstToken = YES; self.channelNameTextField.textDidChangeCallback = self; self.channelNameTextField.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isChannelName == NO) { return TXTLS(@"TDCChannelPropertiesSheet[1nd-7x]"); } return nil; }; [self addConfigurationDidChangeObserver]; [self setupNotificationsController]; } - (void)setupNotificationsController { self.notificationsController.allowsMixedState = YES; NSMutableArray *notifications = [NSMutableArray array]; [notifications addObject:[[TDCChannelPropertiesNotificationConfiguration alloc] initWithEventType:TXNotificationTypeHighlight inSheet:self]]; [notifications addObject:@" "]; [notifications addObject:[[TDCChannelPropertiesNotificationConfiguration alloc] initWithEventType:TXNotificationTypeChannelMessage inSheet:self]]; [notifications addObject:[[TDCChannelPropertiesNotificationConfiguration alloc] initWithEventType:TXNotificationTypeChannelNotice inSheet:self]]; [notifications addObject:@" "]; [notifications addObject:[[TDCChannelPropertiesNotificationConfiguration alloc] initWithEventType:TXNotificationTypeUserJoined inSheet:self]]; [notifications addObject:[[TDCChannelPropertiesNotificationConfiguration alloc] initWithEventType:TXNotificationTypeUserParted inSheet:self]]; self.notificationsController.notifications = notifications; [self.notificationsController attachToView:self.contentViewNotificationsHost]; } - (void)reloadNotificationsController { [self.notificationsController reload]; } - (void)updateNavigationEnabledState { [self.contentViewTabView setEnabled:(self.pushNotificationsCheck.state == NSControlStateValueOn) forSegment:TDCChannelPropertiesSheetSelectionNotifications]; } - (void)loadConfig { self.channelNameTextField.stringValue = self.config.channelName; self.channelNameTextField.editable = (self.config.channelName.length == 0); self.labelTextField.stringValue = self.config.label; self.defaultModesTextField.stringValue = self.config.defaultModes; self.defaultTopicTextField.stringValue = self.config.defaultTopic; self.secretKeyTextField.stringValue = self.config.secretKey; self.autoJoinCheck.state = self.config.autoJoin; self.pushNotificationsCheck.state = self.config.pushNotifications; self.showTreeBadgeCountCheck.state = self.config.showTreeBadgeCount; self.ignoreGeneralEventMessagesCheck.state = self.config.ignoreGeneralEventMessages; self.ignoreHighlightsCheck.state = self.config.ignoreHighlights; self.disableInlineMediaCheck.state = self.config.inlineMediaDisabled; self.enableInlineMediaCheck.state = self.config.inlineMediaEnabled; [self updateNavigationEnabledState]; } - (void)onMenuBarItemChanged:(id)sender { [self _navigateToSelection:[sender indexOfSelectedItem]]; } - (void)navigateToSelection:(TDCChannelPropertiesSheetSelection)selection { if (self.contentViewTabView.indexOfSelectedItem == selection) { return; } [self.contentViewTabView selectSegmentWithTag:selection]; [self _navigateToSelection:selection]; } - (void)_navigateToSelection:(TDCChannelPropertiesSheetSelection)selection { [self selectPane:self.navigationTree[selection][0]]; id firstResponder = self.navigationTree[selection][1]; if ([firstResponder isKindOfClass:[NSControl class]]) { [self.sheet makeFirstResponder:firstResponder]; } } - (void)selectPane:(NSView *)view { [self.contentView replaceFirstSubview:view]; } - (void)start { [self startSheet]; [self _navigateToSelection:TDCChannelPropertiesSheetSelectionGeneral]; } - (void)controlTextDidChange:(NSNotification *)aNotification { if (aNotification.object == self.secretKeyTextField) { [self updateSecretKeyLengthAlert]; } } - (void)updateSecretKeyLengthAlert { NSUInteger maximumKeyLength = self.client.supportInfo.maximumKeyLength; if (maximumKeyLength == 0) { return; } NSUInteger currentKeyLength = self.secretKeyTextField.stringValue.length; if (currentKeyLength <= maximumKeyLength) { return; } if (self.secretKeyLengthAlertDisplayed == NO) { self.secretKeyLengthAlertDisplayed = YES; } else { return; } [TDCAlert alertSheetWithWindow:self.sheet body:TXTLS(@"TDCChannelPropertiesSheet[op4-gg]") title:TXTLS(@"TDCChannelPropertiesSheet[zf2-r7]", self.client.networkNameAlt, maximumKeyLength) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil otherButton:nil suppressionKey:@"maximum_secret_key_length" suppressionText:nil completionBlock:nil]; } - (void)addConfigurationDidChangeObserver { if (self.channel == nil) { return; } [RZNotificationCenter() addObserver:self selector:@selector(underlyingConfigurationChanged:) name:IRCChannelConfigurationWasUpdatedNotification object:self.channel]; } - (void)removeConfigurationDidChangeObserver { [RZNotificationCenter() removeObserver:self]; } - (void)underlyingConfigurationChanged:(NSNotification *)notification { IRCChannel *channel = notification.object; NSWindow *window = self.sheet; [TDCAlert alertSheetWithWindow:window body:TXTLS(@"TDCChannelPropertiesSheet[qby-hi]") title:TXTLS(@"TDCChannelPropertiesSheet[mvl-r5]") defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]") otherButton:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { if (buttonClicked != TDCAlertResponseDefault) { return; } [self close]; self.config = [channel.config copy]; [self loadConfig]; [self reloadNotificationsController]; [self start]; }]; } - (void)onInlineMediaCheckChanged:(id)sender { if (self.enableInlineMediaCheck.state != NSControlStateValueOn) { return; } [TVCLogControllerInlineMediaService askPermissionToEnableInlineMediaWithCompletionBlock:^(BOOL granted) { if (granted == NO) { self.enableInlineMediaCheck.state = NSControlStateValueOff; } }]; } - (void)onPushNotificationsCheckChanged:(id)sender { [self updateNavigationEnabledState]; } #pragma mark - #pragma mark Actions - (void)cancel:(id)sender { [self removeConfigurationDidChangeObserver]; [super cancel:sender]; } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } [self removeConfigurationDidChangeObserver]; self.config.channelName = self.channelNameTextField.value; self.config.label = self.labelTextField.trimmedStringValue; self.config.defaultModes = self.defaultModesTextField.trimmedStringValue; self.config.defaultTopic = self.defaultTopicTextField.trimmedStringValue; self.config.secretKey = self.secretKeyTextField.trimmedFirstTokenStringValue; self.config.autoJoin = self.autoJoinCheck.state; self.config.pushNotifications = self.pushNotificationsCheck.state; self.config.showTreeBadgeCount = self.showTreeBadgeCountCheck.state; self.config.ignoreGeneralEventMessages = self.ignoreGeneralEventMessagesCheck.state; self.config.ignoreHighlights = self.ignoreHighlightsCheck.state; self.config.inlineMediaDisabled = self.disableInlineMediaCheck.state; self.config.inlineMediaEnabled = self.enableInlineMediaCheck.state; if ([self.delegate respondsToSelector:@selector(channelPropertiesSheet:onOk:)]) { [self.delegate channelPropertiesSheet:self onOk:[self.config copy]]; } [super ok:nil]; } - (BOOL)okOrError { return [self okOrErrorForTextField:self.channelNameTextField inSelection:TDCChannelPropertiesSheetSelectionGeneral]; } - (BOOL)okOrErrorForTextField:(TVCValidatedTextField *)textField inSelection:(TDCChannelPropertiesSheetSelection)selection { if (textField.valueIsValid) { return YES; } [self navigateToSelection:selection]; /* Give navigation time to settle before trying to attach popover */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [textField showValidationErrorPopover]; }); return NO; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self.sheet makeFirstResponder:nil]; if ([self.delegate respondsToSelector:@selector(channelPropertiesSheetWillClose:)]) { [self.delegate channelPropertiesSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCHighlightEntrySheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCChannelConfig.h" #import "IRCHighlightMatchCondition.h" #import "TVCValidatedTextField.h" #import "TDCHighlightEntrySheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCHighlightEntrySheet () @property (nonatomic, strong) IRCHighlightMatchConditionMutable *config; @property (nonatomic, copy) NSArray *channelList; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *matchKeywordTextField; @property (nonatomic, weak) IBOutlet NSPopUpButton *matchTypePopupButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *matchChannelPopupButton; @end @implementation TDCHighlightEntrySheet - (instancetype)initWithConfig:(nullable IRCHighlightMatchCondition *)config { if ((self = [super initWithWindow:nil])) { if (config) { self.config = [config mutableCopy]; } else { self.config = [IRCHighlightMatchConditionMutable new]; } [self prepareInitialState]; [self loadConfig]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCHighlightEntrySheet" owner:self topLevelObjects:nil]; self.matchKeywordTextField.stringValueUsesOnlyFirstToken = NO; self.matchKeywordTextField.stringValueIsInvalidOnEmpty = YES; self.matchKeywordTextField.stringValueIsTrimmed = YES; self.matchKeywordTextField.textDidChangeCallback = self; } - (void)loadConfig { self.matchKeywordTextField.stringValue = self.config.matchKeyword; if (self.config.matchIsExcluded == NO) { [self.matchTypePopupButton selectItemWithTag:1]; } else { [self.matchTypePopupButton selectItemWithTag:2]; } } - (void)startWithChannels:(NSArray *)channels { NSParameterAssert(channels != nil); self.channelList = channels; NSString *matchChannelId = self.config.matchChannelId; NSUInteger channelCount = 0; for (IRCChannelConfig *channel in self.channelList) { NSString *channelName = channel.channelName; [self.matchChannelPopupButton addItemWithTitle:channelName]; if ([channel.uniqueIdentifier isEqualToString:matchChannelId]) { [self.matchChannelPopupButton selectItemWithTitle:channelName]; } channelCount += 1; } if (channelCount == 0) { [self.matchChannelPopupButton removeItemAtIndex:1]; } [self startSheet]; [self.sheet makeFirstResponder:self.matchKeywordTextField]; } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } self.config.matchIsExcluded = (self.matchTypePopupButton.selectedTag == 2); self.config.matchKeyword = self.matchKeywordTextField.value; NSInteger selectedChannelIndex = self.matchChannelPopupButton.indexOfSelectedItem; if (selectedChannelIndex > 0) { NSString *selectedChannelName = self.matchChannelPopupButton.titleOfSelectedItem; for (IRCChannelConfig *c in self.channelList) { if ([c.channelName isEqualToString:selectedChannelName]) { self.config.matchChannelId = c.uniqueIdentifier; break; } } } [self.delegate highlightEntrySheet:self onOk:[self.config copy]]; [super ok:sender]; } - (BOOL)okOrError { return [self okOrErrorForTextField:self.matchKeywordTextField]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self.delegate highlightEntrySheetWillClose:self]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCInputPrompt.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCInputPrompt.h" NS_ASSUME_NONNULL_BEGIN @implementation TDCInputPrompt + (TVCAlertResponseButton)promptWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate prefillString:(nullable NSString *)prefillString resultString:(NSString * _Nonnull * _Nonnull )resultString { NSParameterAssert(bodyText != nil); NSParameterAssert(titleText != nil); NSParameterAssert(buttonDefault != nil); /* Create text field */ NSTextField *textField = [NSTextField new]; textField.translatesAutoresizingMaskIntoConstraints = NO; [textField addConstraints: @[ [NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:295.0], [NSLayoutConstraint constraintWithItem:textField attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:22.0] ] ]; textField.editable = YES; textField.selectable = YES; textField.drawsBackground = YES; textField.bordered = YES; textField.bezeled = YES; textField.cell.lineBreakMode = NSLineBreakByTruncatingTail; if (prefillString) { textField.stringValue = prefillString; } /* Create alert */ TVCAlert *alert = [TVCAlert new]; alert.messageText = titleText; alert.informativeText = bodyText; [alert setTitle:buttonDefault forButton:TVCAlertResponseButtonFirst]; [alert setTitle:buttonAlternate forButton:TVCAlertResponseButtonSecond]; alert.accessoryView = textField; alert.window.initialFirstResponder = textField; /* Run modal */ TVCAlertResponseButton response = [alert runModal]; /* Assign result */ *resultString = textField.stringValue; /* Return response */ return response; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCNicknameColorSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCUserNicknameColorStyleGeneratorPrivate.h" #import "TDCNicknameColorSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCNicknameColorSheet () @property (nonatomic, copy) NSString *nickname; @property (nonatomic, weak) IBOutlet NSColorWell *nicknameColorWell; - (IBAction)resetNicknameColor:(id)sender; @end @implementation TDCNicknameColorSheet - (instancetype)initWithNickname:(NSString *)nickname { NSParameterAssert(nickname != nil); if ((self = [super initWithWindow:nil])) { self.nickname = nickname; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCNicknameColorSheet" owner:self topLevelObjects:nil]; NSColor *nicknameColor = [IRCUserNicknameColorStyleGenerator nicknameColorStyleOverrideForKey:self.nickname]; if (nicknameColor == nil) { nicknameColor = [NSColor whiteColor]; } self.nicknameColorWell.color = nicknameColor; } - (void)start { [self startSheet]; } - (void)ok:(id)sender { NSColor *nicknameColor = self.nicknameColorWell.color; if ([nicknameColor isEqual:[NSColor whiteColor]]) { nicknameColor = nil; } [IRCUserNicknameColorStyleGenerator setNicknameColorStyleOverride:nicknameColor forKey:self.nickname]; if ([self.delegate respondsToSelector:@selector(nicknameColorSheetOnOk:)]) { [self.delegate nicknameColorSheetOnOk:self]; } [super ok:nil]; } - (void)resetNicknameColor:(id)sender { if ([NSColorPanel sharedColorPanelExists]) { [[NSColorPanel sharedColorPanel] close]; } self.nicknameColorWell.color = [NSColor whiteColor]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { if ([self.delegate respondsToSelector:@selector(nicknameColorSheetWillClose:)]) { [self.delegate nicknameColorSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCProgressIndicatorSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TDCProgressIndicatorSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCProgressIndicatorSheet () @property (nonatomic, weak) IBOutlet NSProgressIndicator *progressIndicator; @end @implementation TDCProgressIndicatorSheet - (instancetype)initWithWindow:(nullable NSWindow *)window { NSParameterAssert(window != nil); if ((self = [super initWithWindow:window])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCProgressIndicatorSheet" owner:self topLevelObjects:nil]; } - (void)start { [self.progressIndicator startAnimation:nil]; [self startSheet]; } - (void)stop { [self.progressIndicator stopAnimation:nil]; [self close]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCServerChangeNicknameSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "IRCClient.h" #import "TLOLocalization.h" #import "TVCValidatedTextField.h" #import "TDCServerChangeNicknameSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCServerChangeNicknameSheet () @property (nonatomic, strong, readwrite) IRCClient *client; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *tnewNicknameTextField; @property (nonatomic, weak) IBOutlet NSTextField *toldNicknameTextField; @end @implementation TDCServerChangeNicknameSheet - (instancetype)initWithClient:(IRCClient *)client { NSParameterAssert(client != nil); if ((self = [super initWithWindow:nil])) { self.client = client; self.clientId = client.uniqueIdentifier; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCServerChangeNicknameSheet" owner:self topLevelObjects:nil]; self.tnewNicknameTextField.stringValueIsInvalidOnEmpty = YES; self.tnewNicknameTextField.stringValueUsesOnlyFirstToken = YES; self.tnewNicknameTextField.textDidChangeCallback = self; self.tnewNicknameTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([currentValue isHostmaskNicknameOn:self.client] == NO) { return TXTLS(@"CommonErrors[och-j5]"); } return nil; }; NSString *nickname = self.client.userNickname; self.tnewNicknameTextField.stringValue = nickname; self.toldNicknameTextField.stringValue = nickname; } - (void)start { [self startSheet]; [self.sheet makeFirstResponder:self.tnewNicknameTextField]; } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } if ([self.delegate respondsToSelector:@selector(serverChangeNicknameSheet:didInputNickname:)]) { NSString *newNickname = self.tnewNicknameTextField.value; [self.delegate serverChangeNicknameSheet:self didInputNickname:newNickname]; } [super ok:sender]; } - (BOOL)okOrError { return [self okOrErrorForTextField:self.tnewNicknameTextField]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { if ([self.delegate respondsToSelector:@selector(serverChangeNicknameSheetWillClose:)]) { [self.delegate serverChangeNicknameSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCServerChannelListDialog.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSColorHelper.h" #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "TXGlobalModels.h" #import "NSTableViewHelperPrivate.h" #import "TLOLocalization.h" #import "IRCClient.h" #import "TVCBasicTableView.h" #import "TDCServerChannelListDialogPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCServerChannelListDialogEntry : NSObject @property (nonatomic, copy) NSString *channelName; @property (nonatomic, copy) NSNumber *channelMemberCount; @property (nonatomic, copy) NSString *channelTopicUnformatted; @property (nonatomic, copy) NSAttributedString *channelTopicFormatted; @end @interface TDCServerChannelListDialog () @property (nonatomic, strong, readwrite) IRCClient *client; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, assign) BOOL isWaitingForWrites; @property (nonatomic, strong) NSMutableArray *queuedWrites; @property (nonatomic, weak) IBOutlet NSButton *updateButton; @property (nonatomic, weak) IBOutlet NSSearchField *searchTextField; @property (nonatomic, weak) IBOutlet NSTextField *networkNameTextField; @property (nonatomic, weak) IBOutlet TVCBasicTableView *channelListTable; @property (nonatomic, strong) IBOutlet NSArrayController *channelListController; - (IBAction)onClose:(id)sender; - (IBAction)onUpdate:(id)sender; - (IBAction)onJoinChannels:(id)sender; @end @implementation TDCServerChannelListDialog - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithClient:(IRCClient *)client { NSParameterAssert(client != nil); if ((self = [super init])) { self.client = client; self.clientId = client.uniqueIdentifier; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCServerChannelListDialog" owner:self topLevelObjects:nil]; self.queuedWrites = [NSMutableArray array]; self.channelListTable.doubleAction = @selector(onJoin:); self.channelListTable.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"channelMemberCount" ascending:NO selector:@selector(compare:)] ]; self.networkNameTextField.stringValue = TXTLS(@"TDCServerChannelListDialog[7qf-r0]", self.client.networkNameAlt); } - (void)show { [self.window restoreWindowStateForClass:self.class]; [super show]; } - (void)clear { self.channelListController.content = nil; [self updateDialogTitle]; } - (void)addChannel:(NSString *)channel count:(NSUInteger)count topic:(nullable NSString *)topic { NSParameterAssert(channel != nil); TDCServerChannelListDialogEntry *newEntry = [TDCServerChannelListDialogEntry new]; newEntry.channelName = channel; newEntry.channelMemberCount = @(count); if (topic == nil) { newEntry.channelTopicUnformatted = @""; newEntry.channelTopicFormatted = [NSAttributedString attributedString]; } else { newEntry.channelTopicUnformatted = topic; NSAttributedString *topicFormatted = [topic attributedStringWithIRCFormatting:[NSTableView preferredGlobalTableViewFont] preferredFontColor:[NSColor controlTextColor]]; newEntry.channelTopicFormatted = topicFormatted; } @synchronized(self.queuedWrites) { [self.queuedWrites addObject:newEntry]; } if (self.isWaitingForWrites == NO) { self.isWaitingForWrites = YES; [self performSelectorInCommonModes:@selector(queuedWritesTimer) withObject:nil afterDelay:1.0]; } } - (void)queuedWritesTimer { self.isWaitingForWrites = NO; [self writeQueuedWrites]; } - (void)writeQueuedWrites { @synchronized(self.queuedWrites) { if (self.queuedWrites.count == 0) { return; } NSPredicate *filterPredicate = self.channelListController.filterPredicate; if (filterPredicate) { NSMutableArray *queuedWrites = [NSMutableArray array]; for (TDCServerChannelListDialogEntry *queuedWrite in self.queuedWrites) { if ([filterPredicate evaluateWithObject:queuedWrite]) { [queuedWrites addObject:queuedWrite]; } } [self.channelListController addObjects:queuedWrites]; [self.queuedWrites removeObjectsInArray:queuedWrites]; } else { [self.channelListController addObjects:self.queuedWrites]; [self.queuedWrites removeAllObjects]; } } [self updateDialogTitle]; } - (void)controlTextDidChange:(NSNotification *)obj { if (obj.object == self.searchTextField) { NSString *currentSearchValue = self.searchTextField.stringValue; if (currentSearchValue.length == 0) { [self writeQueuedWrites]; } } } - (void)updateDialogTitle { id arrangedObjects = self.channelListController.arrangedObjects; NSString *arrangedObjectsCount = TXFormattedNumber([arrangedObjects count]); self.window.title = TXTLS(@"TDCServerChannelListDialog[ct4-wh]", arrangedObjectsCount); } #pragma mark - #pragma mark Actions - (void)onClose:(id)sender { [self close]; } - (void)onUpdate:(id)sender { [self clear]; if ([self.delegate respondsToSelector:@selector(serverChannelListDialogOnUpdate:)]) { [self.delegate serverChannelListDialogOnUpdate:self]; } } /* onJoinChannels: handles join for selected items. */ - (void)onJoinChannels:(id)sender { [self onJoin:sender]; } /* onJoin: is a legacy method. It handles join on double click. */ - (void)onJoin:(id)sender { NSIndexSet *selectedRows = self.channelListTable.selectedRowIndexes; NSMutableArray *channelNames = [NSMutableArray arrayWithCapacity:selectedRows.count]; [selectedRows enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { TDCServerChannelListDialogEntry *channelEntry = self.channelListController.arrangedObjects[index]; [channelNames addObject:channelEntry.channelName]; }]; if ([self.delegate respondsToSelector:@selector(serverChannelListDialog:joinChannels:)]) { [self.delegate serverChannelListDialog:self joinChannels:[channelNames copy]]; } [self.channelListTable deselectAll:nil]; } #pragma mark - #pragma mark NSTableViewDelegate - (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes { #define _maximumSelectedRows 8 return [tableView selectionIndexesForProposedSelection:proposedSelectionIndexes maximumNumberOfSelections:_maximumSelectedRows]; #undef _maximumSelectedRows } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { [self cancelPerformRequests]; self.channelListTable.dataSource = nil; self.channelListTable.delegate = nil; [self.window saveWindowStateForClass:self.class]; if ([self.delegate respondsToSelector:@selector(serverChannelDialogWillClose:)]) { [self.delegate serverChannelDialogWillClose:self]; } } @end #pragma mark - @implementation TDCServerChannelListDialogEntry @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCServerHighlightListSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCHighlightLogEntryPrivate.h" #import "TVCBasicTableView.h" #import "TVCMainWindow.h" #import "TDCServerHighlightListSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _renderedMessageTextFieldLeftRightPadding 2.0 @interface TDCServerHighlightListSheet () @property (nonatomic, strong, readwrite) IRCClient *client; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, weak) IBOutlet NSTextField *headerTitleTextField; @property (nonatomic, weak) IBOutlet TVCBasicTableView *highlightListTable; @property (nonatomic, strong) IBOutlet NSArrayController *highlightListController; - (IBAction)onClearList:(id)sender; @end @implementation TDCServerHighlightListSheet - (instancetype)initWithClient:(IRCClient *)client { NSParameterAssert(client != nil); if ((self = [super initWithWindow:nil])) { self.client = client; self.clientId = client.uniqueIdentifier; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCServerHighlightListSheet" owner:self topLevelObjects:nil]; self.highlightListTable.doubleAction = @selector(highlightDoubleClicked:); self.highlightListTable.pasteboardDelegate = self; self.highlightListTable.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"timeLogged" ascending:NO selector:@selector(compare:)], [NSSortDescriptor sortDescriptorWithKey:@"channelName" ascending:NO selector:@selector(caseInsensitiveCompare:)] ]; NSString *headerTitle = [NSString stringWithFormat:self.headerTitleTextField.stringValue, self.client.networkNameAlt]; self.headerTitleTextField.stringValue = headerTitle; NSArray *cachedHighlights = self.client.cachedHighlights; if (cachedHighlights) { [self addEntry:cachedHighlights]; } } - (void)start { [self startSheet]; } - (void)addEntry:(id)newEntry { NSParameterAssert(newEntry != nil); if ([newEntry isKindOfClass:[NSArray class]]) { for (id entry in newEntry) { [self addEntry:entry]; } } else if ([newEntry isKindOfClass:[IRCHighlightLogEntry class]]) { if ([newEntry isKindOfClass:[IRCHighlightLogEntryMutable class]]) { newEntry = [newEntry copy]; } [self.highlightListController addObject:newEntry]; } } - (void)onClearList:(id)sender { self.highlightListController.content = nil; [self.client clearCachedHighlights]; } - (void)highlightDoubleClicked:(id)sender { NSInteger row = self.highlightListTable.clickedRow; if (row < 0) { return; } IRCHighlightLogEntry *entryItem = self.highlightListController.arrangedObjects[row]; IRCChannel *channel = entryItem.channel; if (channel == nil) { return; } TVCLogController *viewController = channel.viewController; [viewController jumpToLine:entryItem.lineNumber completionHandler:^(BOOL result) { if (result) { [mainWindow() select:channel]; [self cancel:nil]; } }]; } - (void)copy:(id)sender { NSIndexSet *selectedRows = self.highlightListTable.selectedRowIndexes; if (selectedRows.count == 0) { return; } NSMutableString *stringToCopy = [NSMutableString string]; [selectedRows enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { IRCHighlightLogEntry *entryItem = self.highlightListController.arrangedObjects[index]; [stringToCopy appendString:entryItem.description]; if (index != selectedRows.lastIndex) { [stringToCopy appendString:@"\n"]; } }]; [RZPasteboard() setStringContent:stringToCopy]; } #pragma mark - #pragma mark NSTableView Delegate - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { NSTableCellView *result = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self]; return result; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { self.highlightListTable.dataSource = nil; self.highlightListTable.delegate = nil; if ([self.delegate respondsToSelector:@selector(serverHighlightListSheetWillClose:)]) { [self.delegate serverHighlightListSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCServerPropertiesSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "NSStringHelper.h" #import "IRCClientConfigPrivate.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCHighlightMatchCondition.h" #import "IRCNetworkList.h" #import "IRCServer.h" #import "TLOLocalization.h" #import "TLOpenLink.h" #import "TPCPreferencesLocal.h" #import "TPCPreferencesUserDefaults.h" #import "TVCBasicTableView.h" #import "TVCValidatedComboBox.h" #import "TVCContentNavigationOutlineViewPrivate.h" #import "TVCValidatedTextField.h" #import "TDCAddressBookSheetPrivate.h" #import "TDCAlert.h" #import "TDCChannelPropertiesSheetPrivate.h" #import "TDCHighlightEntrySheetPrivate.h" #import "TDCPreferencesControllerPrivate.h" #import "TDCServerEndpointListSheetPrivate.h" #import "TDCServerPropertiesSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _tableDragToken @"TDCServerPropertiesSheetTableDragToken" @interface TDCServerPropertiesSheet () @property (nonatomic, strong, readwrite, nullable) IRCClient *client; @property (nonatomic, copy, readwrite, nullable) NSString *clientId; @property (nonatomic, strong) IRCClientConfigMutable *config; @property (nonatomic, copy) NSArray *navigationTreeMatrix; @property (nonatomic, copy) NSDictionary *encodingList; @property (nonatomic, strong) IRCNetworkList *networkList; @property (nonatomic, strong) IBOutlet NSMenu *addAddressBookEntryMenu; @property (nonatomic, strong) IBOutlet NSView *contentViewAddressBook; @property (nonatomic, strong) IBOutlet NSView *contentViewAutojoin; @property (nonatomic, strong) IBOutlet NSView *contentViewClientCertificate; @property (nonatomic, strong) IBOutlet NSView *contentViewConnectCommands; @property (nonatomic, strong) IBOutlet NSView *contentViewDisconnectMessages; @property (nonatomic, strong) IBOutlet NSView *contentViewEncoding; @property (nonatomic, strong) IBOutlet NSView *contentViewFloodControl; @property (nonatomic, strong) IBOutlet NSView *contentViewGeneral; @property (nonatomic, strong) IBOutlet NSView *contentViewHighlights; @property (nonatomic, strong) IBOutlet NSView *contentViewIdentity; @property (nonatomic, strong) IBOutlet NSView *contentViewNetworkSocket; @property (nonatomic, strong) IBOutlet NSView *contentViewProxyServer; @property (nonatomic, strong) IBOutlet NSView *contentViewProxyServerInputView; @property (nonatomic, strong) IBOutlet NSView *contentViewProxyServerSystemSocksView; @property (nonatomic, strong) IBOutlet NSView *contentViewProxyServerTorBrowserView; @property (nonatomic, strong) IBOutlet NSView *contentViewRedundancy; @property (nonatomic, strong) IBOutlet NSView *contentViewZncBouncer; @property (nonatomic, strong, nullable) TDCAddressBookSheet *addressBookSheet; @property (nonatomic, strong, nullable) TDCHighlightEntrySheet *highlightSheet; @property (nonatomic, strong, nullable) TDCChannelPropertiesSheet *channelSheet; @property (nonatomic, strong, nullable) TDCServerEndpointListSheet *serverEndpointSheet; @property (nonatomic, weak) IBOutlet NSButton *addAddressBookEntryButton; @property (nonatomic, weak) IBOutlet NSButton *addChannelButton; @property (nonatomic, weak) IBOutlet NSButton *addHighlightButton; @property (nonatomic, weak) IBOutlet NSButton *autoConnectCheck; @property (nonatomic, weak) IBOutlet NSButton *autoDisconnectOnSleepCheck; @property (nonatomic, weak) IBOutlet NSButton *autoReconnectCheck; @property (nonatomic, weak) IBOutlet NSButton *autojoinWaitsForNickServCheck; @property (nonatomic, weak) IBOutlet NSButton *clientCertificateChangeCertificateButton; @property (nonatomic, weak) IBOutlet NSButton *clientCertificateResetCertificateButton; @property (nonatomic, weak) IBOutlet NSButton *clientCertificateMD5FingerprintCopyButton; @property (nonatomic, weak) IBOutlet NSButton *clientCertificateSHA1FingerprintCopyButton; @property (nonatomic, weak) IBOutlet NSButton *clientCertificateSHA2FingerprintCopyButton; @property (nonatomic, weak) IBOutlet NSButton *clientCertificateSHA512FingerprintCopyButton; @property (nonatomic, weak) IBOutlet NSButton *connectionIPv4AddressTypeCheck; @property (nonatomic, weak) IBOutlet NSButton *connectionIPv6AddressTypeCheck; @property (nonatomic, weak) IBOutlet NSButton *connectionDefaultAddressTypeCheck; @property (nonatomic, weak) IBOutlet NSButton *deleteAddressBookEntryButton; @property (nonatomic, weak) IBOutlet NSButton *deleteChannelButton; @property (nonatomic, weak) IBOutlet NSButton *deleteHighlightButton; @property (nonatomic, weak) IBOutlet NSButton *disconnectOnReachabilityChangeCheck; @property (nonatomic, weak) IBOutlet NSButton *editAddressBookEntryButton; @property (nonatomic, weak) IBOutlet NSButton *editChannelButton; @property (nonatomic, weak) IBOutlet NSButton *editHighlightButton; @property (nonatomic, weak) IBOutlet NSButton *hideAutojoinDelayedWarningsCheck; @property (nonatomic, weak) IBOutlet NSButton *performDisconnectOnPongTimerCheck; @property (nonatomic, weak) IBOutlet NSButton *pongTimerCheck; @property (nonatomic, weak) IBOutlet NSButton *prefersSecuredConnectionCheck; @property (nonatomic, weak) IBOutlet NSButton *setInvisibleModeOnConnectCheck; @property (nonatomic, weak) IBOutlet NSButton *validateServerCertificateChainCheck; @property (nonatomic, weak) IBOutlet NSButton *viewListOfPreferredCipherSuitesButton; @property (nonatomic, weak) IBOutlet NSButton *zncIgnoreConfiguredAutojoinCheck; @property (nonatomic, weak) IBOutlet NSButton *zncIgnorePlaybackNotificationsCheck; @property (nonatomic, weak) IBOutlet NSButton *zncOnlyPlaybackLatestCheck; @property (nonatomic, weak) IBOutlet NSPopUpButton *fallbackEncodingButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *primaryEncodingButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *preferredCipherSuitesButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *proxyTypeButton; @property (nonatomic, weak) IBOutlet NSSlider *floodControlDelayTimerSlider; @property (nonatomic, weak) IBOutlet NSSlider *floodControlMessageCountSlider; @property (nonatomic, weak) IBOutlet NSTextField *clientCertificateCommonNameField; @property (nonatomic, weak) IBOutlet NSTextField *clientCertificateMD5FingerprintField; @property (nonatomic, weak) IBOutlet NSTextField *clientCertificateSHA1FingerprintField; @property (nonatomic, weak) IBOutlet NSTextField *clientCertificateSHA2FingerprintField; @property (nonatomic, weak) IBOutlet NSTextField *clientCertificateSHA512FingerprintField; @property (nonatomic, weak) IBOutlet NSTextField *nicknamePasswordTextField; @property (nonatomic, weak) IBOutlet NSTextField *proxyPasswordTextField; @property (nonatomic, weak) IBOutlet NSTextField *proxyUsernameTextField; @property (nonatomic, weak) IBOutlet NSTextField *serverPasswordTextField; @property (nonatomic, weak) IBOutlet TVCBasicTableView *addressBookTable; @property (nonatomic, weak) IBOutlet TVCBasicTableView *channelListTable; @property (nonatomic, weak) IBOutlet TVCBasicTableView *highlightsTable; @property (nonatomic, weak) IBOutlet TVCValidatedComboBox *serverAddressComboBox; @property (nonatomic, weak) IBOutlet TVCContentNavigationOutlineView *navigationOutlineView; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *alternateNicknamesTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *awayNicknameTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *connectionNameTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *nicknameTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *normalLeavingCommentTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *proxyAddressTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *proxyPortTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *realNameTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *serverPortTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *sleepModeQuitMessageTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *usernameTextField; @property (nonatomic, unsafe_unretained) IBOutlet NSTextView *connectCommandsField; @property (nonatomic, assign) NSUInteger floodControlDelayTimerSliderTempValue; @property (nonatomic, assign) NSUInteger floodControlMessageCountSliderTempValue; @property (nonatomic, weak) NSPanel *clientCertificateSelectCertificatePanel; @property (nonatomic, strong) IBOutlet NSArrayController *addressBookArrayController; @property (nonatomic, strong) IBOutlet NSArrayController *channelListArrayController; @property (nonatomic, strong) IBOutlet NSArrayController *highlightListArrayController; @property (nonatomic, strong) IBOutlet NSArrayController *serverListArrayController; @property (readonly, copy) NSArray *addressBookList; @property (readonly, copy) NSArray *channelList; @property (readonly, copy) NSArray *highlightList; @property (readonly, copy) NSArray *serverList; @property (nonatomic, assign) BOOL populatingPrimaryServer; @property (nonatomic, assign) BOOL primaryServerIsPredefined; @property (nonatomic, copy, nullable) NSString *lastServerAddressValue; @property (nonatomic, copy, nullable) IRCServer *previousPrimaryServer; - (IBAction)proxyTypeChanged:(id)sender; - (IBAction)toggleAdvancedEncodings:(id)sender; - (IBAction)addChannel:(id)sender; - (IBAction)editChannel:(id)sender; - (IBAction)deleteChannel:(id)sender; - (IBAction)addHighlight:(id)sender; - (IBAction)editHighlight:(id)sender; - (IBAction)deleteHighlight:(id)sender; - (IBAction)addAddressBookEntry:(id)sender; - (IBAction)editAddressBookEntry:(id)sender; - (IBAction)deleteAddressBookEntry:(id)sender; - (IBAction)showAddAddressBookEntryMenu:(id)sender; - (IBAction)openProxySettingsInSystemPreferences:(id)sender; - (IBAction)editSeverEndpoints:(id)sender; - (IBAction)useSSLCheckChanged:(id)sender; - (IBAction)autojoinWaitsForNickServChanged:(id)sender; - (IBAction)onClientCertificateResetRequested:(id)sender; - (IBAction)onClientCertificateChangeRequested:(id)sender; - (IBAction)onClientCertificateFingerprintSHA512CopyRequested:(id)sender; - (IBAction)onClientCertificateFingerprintSHA2CopyRequested:(id)sender; - (IBAction)onClientCertificateFingerprintSHA1CopyRequested:(id)sender; - (IBAction)onClientCertificateFingerprintMD5CopyRequested:(id)sender; - (IBAction)preferredCipherSuitesChanged:(id)sender; - (IBAction)preferredCipherSuitesViewList:(id)sender; - (IBAction)preferredInternetProtocolChanged:(id)sender; @end #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wincomplete-implementation" @implementation TDCServerPropertiesSheet - (instancetype)initWithClient:(nullable IRCClient *)client { if ((self = [super initWithWindow:nil])) { if (client) { self.client = client; self.clientId = client.uniqueIdentifier; [client updateStoredConfiguration]; self.config = [client.config mutableCopy]; } else { self.config = [IRCClientConfigMutable new]; } [self prepareInitialState]; [self loadConfig]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCServerPropertiesSheet" owner:self topLevelObjects:nil]; /* Populate server list combo box */ self.networkList = [IRCNetworkList new]; NSArray *listOfNetworks = self.networkList.listOfNetworks; for (IRCNetwork *network in listOfNetworks) { [self.serverAddressComboBox addItemWithObjectValue:network.networkName]; } /* Connect commands text box better font */ self.connectCommandsField.font = [NSFont systemFontOfSize:13.0]; self.connectCommandsField.textContainerInset = NSMakeSize(1, 3); /* Alternate nicknameS */ self.alternateNicknamesTextField.textDidChangeCallback = self; self.alternateNicknamesTextField.stringValueIsInvalidOnEmpty = NO; self.alternateNicknamesTextField.stringValueIsTrimmed = YES; self.alternateNicknamesTextField.validationBlock = ^NSString *(NSString *currentValue) { NSArray *nicknames = [currentValue componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *nickname in nicknames) { if (nickname.isHostmaskNickname == NO) { return TXTLS(@"TDCServerPropertiesSheet[wlz-tb]", nickname); } } return nil; }; /* Away nickname */ self.awayNicknameTextField.textDidChangeCallback = self; self.awayNicknameTextField.stringValueIsInvalidOnEmpty = NO; self.awayNicknameTextField.stringValueIsTrimmed = YES; self.awayNicknameTextField.stringValueUsesOnlyFirstToken = YES; self.awayNicknameTextField.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isHostmaskNickname == NO) { return TXTLS(@"CommonErrors[och-j5]"); } return nil; }; /* Nickname */ self.nicknameTextField.textDidChangeCallback = self; self.nicknameTextField.stringValueIsInvalidOnEmpty = YES; self.nicknameTextField.stringValueIsTrimmed = YES; self.nicknameTextField.stringValueUsesOnlyFirstToken = YES; self.nicknameTextField.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isHostmaskNickname == NO) { return TXTLS(@"CommonErrors[och-j5]"); } return nil; }; /* Username */ self.usernameTextField.textDidChangeCallback = self; self.usernameTextField.stringValueIsInvalidOnEmpty = YES; self.usernameTextField.stringValueIsTrimmed = YES; self.usernameTextField.stringValueUsesOnlyFirstToken = YES; self.usernameTextField.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isHostmaskUsername == NO) { return TXTLS(@"TDCServerPropertiesSheet[8iw-q8]"); } return nil; }; /* Real name */ self.realNameTextField.textDidChangeCallback = self; self.realNameTextField.stringValueIsInvalidOnEmpty = YES; self.realNameTextField.stringValueIsTrimmed = YES; self.realNameTextField.stringValueUsesOnlyFirstToken = NO; self.realNameTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([currentValue containsCharactersFromCharacterSet:[NSCharacterSet newlineCharacterSet]]) { return TXTLS(@"TDCServerPropertiesSheet[agy-bp]"); } return nil; }; /* Normal leaving comment */ self.normalLeavingCommentTextField.textDidChangeCallback = self; self.normalLeavingCommentTextField.stringValueIsInvalidOnEmpty = NO; self.normalLeavingCommentTextField.stringValueIsTrimmed = YES; self.normalLeavingCommentTextField.stringValueUsesOnlyFirstToken = NO; self.normalLeavingCommentTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([currentValue containsCharactersFromCharacterSet:[NSCharacterSet newlineCharacterSet]]) { return TXTLS(@"CommonErrors[gas-v8]"); } if (currentValue.length > 390) { return TXTLS(@"CommonErrors[2cb-af]", 390); } return nil; }; /* Sleep mode leaving comment */ self.sleepModeQuitMessageTextField.textDidChangeCallback = self; self.sleepModeQuitMessageTextField.stringValueIsInvalidOnEmpty = NO; self.sleepModeQuitMessageTextField.stringValueIsTrimmed = YES; self.sleepModeQuitMessageTextField.stringValueUsesOnlyFirstToken = NO; self.sleepModeQuitMessageTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([currentValue containsCharactersFromCharacterSet:[NSCharacterSet newlineCharacterSet]]) { return TXTLS(@"CommonErrors[gas-v8]"); } if (currentValue.length > 390) { return TXTLS(@"CommonErrors[2cb-af]", 390); } return nil; }; /* Connection name */ self.connectionNameTextField.textDidChangeCallback = self; self.connectionNameTextField.stringValueIsInvalidOnEmpty = YES; self.connectionNameTextField.stringValueIsTrimmed = YES; self.connectionNameTextField.stringValueUsesOnlyFirstToken = NO; self.connectionNameTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([currentValue containsCharactersFromCharacterSet:[NSCharacterSet newlineCharacterSet]]) { return TXTLS(@"CommonErrors[gas-v8]"); } return nil; }; /* Server address */ self.serverAddressComboBox.textDidChangeCallback = self; self.serverAddressComboBox.stringValueIsInvalidOnEmpty = YES; self.serverAddressComboBox.stringValueIsTrimmed = YES; self.serverAddressComboBox.stringValueUsesOnlyFirstToken = YES; self.serverAddressComboBox.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isValidInternetAddress == NO) { return TXTLS(@"CommonErrors[yyx-l3]"); } return nil; }; /* Server port */ self.serverPortTextField.textDidChangeCallback = self; self.serverPortTextField.stringValueIsInvalidOnEmpty = YES; self.serverPortTextField.stringValueIsTrimmed = YES; self.serverPortTextField.stringValueUsesOnlyFirstToken = NO; self.serverPortTextField.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isValidInternetPort == NO) { return TXTLS(@"CommonErrors[l0c-nb]"); } return nil; }; /* Proxy address */ self.proxyAddressTextField.textDidChangeCallback = self; self.proxyAddressTextField.stringValueIsInvalidOnEmpty = NO; self.proxyAddressTextField.stringValueIsTrimmed = YES; self.proxyAddressTextField.stringValueUsesOnlyFirstToken = YES; self.proxyAddressTextField.performValidationWhenEmpty = YES; self.proxyAddressTextField.validationBlock = ^NSString *(NSString *currentValue) { NSInteger proxyType = self.proxyTypeButton.selectedTag; if (proxyType == IRCConnectionProxyTypeSocks4 || proxyType == IRCConnectionProxyTypeSocks5 || proxyType == IRCConnectionProxyTypeHTTP || proxyType == IRCConnectionProxyTypeHTTPS) { if (currentValue.isValidInternetAddress == NO) { return TXTLS(@"TDCServerPropertiesSheet[tlo-b6]"); } } return nil; }; /* Proxy port */ self.proxyPortTextField.textDidChangeCallback = self; self.proxyPortTextField.stringValueIsInvalidOnEmpty = NO; self.proxyPortTextField.stringValueIsTrimmed = YES; self.proxyPortTextField.stringValueUsesOnlyFirstToken = NO; self.proxyPortTextField.performValidationWhenEmpty = YES; self.proxyPortTextField.defaultValue = [NSString stringWithUnsignedInteger:IRCConnectionDefaultProxyPort]; self.proxyPortTextField.validationBlock = ^NSString *(NSString *currentValue) { NSInteger proxyType = self.proxyTypeButton.selectedTag; if (proxyType == IRCConnectionProxyTypeSocks4 || proxyType == IRCConnectionProxyTypeSocks5 || proxyType == IRCConnectionProxyTypeHTTP || proxyType == IRCConnectionProxyTypeHTTPS) { if (currentValue.isValidInternetPort == NO) { return TXTLS(@"CommonErrors[l0c-nb]"); } } return nil; }; /* Setup others */ [self addConfigurationDidChangeObserver]; self.addressBookTable.doubleAction = @selector(tableViewDoubleClicked:); self.addressBookTable.target = self; [self.addressBookTable registerForDraggedTypes:@[_tableDragToken]]; self.channelListTable.doubleAction = @selector(tableViewDoubleClicked:); self.channelListTable.target = self; [self.channelListTable registerForDraggedTypes:@[_tableDragToken]]; self.highlightsTable.doubleAction = @selector(tableViewDoubleClicked:); self.highlightsTable.target = self; [self.highlightsTable registerForDraggedTypes:@[_tableDragToken]]; [self populateEncodings]; [self populateTabViewList]; } - (void)dealloc { self.connectCommandsField = nil; } - (void)populateTabViewList { #define _groupItem(_label_, _children_) \ [[TVCContentNavigationOutlineViewItem alloc] initWithLabel:TXTLS(_label_) identifier:0 view:nil firstResponder:nil children:(_children_)] #define _childItem(_label_, _identifier_) \ [[TVCContentNavigationOutlineViewItem alloc] initWithLabel:TXTLS(_label_) identifier:TDCServerPropertiesSheetSelection ##_identifier_ view:self.contentView ##_identifier_ firstResponder:nil] NSArray *generalSectionChildren = @[ _childItem(@"TDCServerPropertiesSheet[8zc-6y]", AddressBook), _childItem(@"TDCServerPropertiesSheet[5oz-07]", Autojoin), _childItem(@"TDCServerPropertiesSheet[hip-13]", ConnectCommands), _childItem(@"TDCServerPropertiesSheet[8ug-ka]", Encoding), _childItem(@"TDCServerPropertiesSheet[ehx-4d]", General), _childItem(@"TDCServerPropertiesSheet[8ik-qo]", Identity), _childItem(@"TDCServerPropertiesSheet[jtx-hn]", Highlights), _childItem(@"TDCServerPropertiesSheet[j34-yr]", DisconnectMessages), ]; NSArray *vendorSectionChildren = @[ _childItem(@"TDCServerPropertiesSheet[fsj-7f]", ZncBouncer) ]; NSArray *advancedSectionChildren = @[ _childItem(@"TDCServerPropertiesSheet[ce7-kc]", ClientCertificate), _childItem(@"TDCServerPropertiesSheet[fcr-w8]", FloodControl), _childItem(@"TDCServerPropertiesSheet[ffy-xt]", NetworkSocket), _childItem(@"TDCServerPropertiesSheet[t52-7a]", ProxyServer), _childItem(@"TDCServerPropertiesSheet[36n-u9]", Redundancy) ]; NSArray *navigationTreeMatrix = @[ _groupItem(@"TDCServerPropertiesSheet[lww-pc]", generalSectionChildren), _groupItem(@"TDCServerPropertiesSheet[v27-8w]", vendorSectionChildren), _groupItem(@"TDCServerPropertiesSheet[8uw-tz]", advancedSectionChildren) ]; #undef _groupItem #undef _childItem self.navigationOutlineView.navigationTreeMatrix = navigationTreeMatrix; /* Auto layout will grow view beyond these values. We are just declaring the default which doesn't matter much. */ self.navigationOutlineView.contentViewPreferredWidth = 100; self.navigationOutlineView.contentViewPreferredHeight = 100; self.navigationOutlineView.expandParentOnDoubleClick = YES; [self.navigationOutlineView expandItem:navigationTreeMatrix[0]]; } - (void)populateEncodings { [self.primaryEncodingButton removeAllItems]; [self.fallbackEncodingButton removeAllItems]; /* Build list of encodings */ self.encodingList = [NSString supportedStringEncodingsWithTitle:NO]; NSArray *encodingNames = self.encodingList.sortedDictionaryKeys; /* What we are basically doing now is sorting all the encodings, then removing UTF-8 from the sorted list and inserting it at the top of the list. */ NSString *utf8title = [NSString localizedNameOfStringEncoding:NSUTF8StringEncoding]; NSMutableArray *encodingsToAdd = [encodingNames mutableCopy]; [encodingsToAdd removeObject:utf8title]; [self.primaryEncodingButton addItemWithTitle:utf8title]; [self.fallbackEncodingButton addItemWithTitle:utf8title]; /* Add the encodings to the popup list. This for loop will find the first parentheses opening and compare everything before it to the one found for the previous encoding. If the prefix has changed, then a separator is inserted. This groups the encodings.*/ /* We do this two times. The first time setups up preferred encodings at the top of the list. The next handles everything else. */ NSArray *favoredEncodings = @[@"Unicode", @"Western", @"Central European"]; [self populateEncodingPopup:encodingsToAdd preferredEncodings:favoredEncodings ignoreFavored:NO]; if ([RZUserDefaults() boolForKey:@"Server Properties Window Sheet -> Include Advanced Encodings"]) { [self populateEncodingPopup:encodingsToAdd preferredEncodings:favoredEncodings ignoreFavored:YES]; } } - (void)populateEncodingPopup:(NSArray *)encodingsToAdd preferredEncodings:(NSArray *)favoredEncodings ignoreFavored:(BOOL)ignoreFavored { NSString *encodingPrefixPrevious = nil; for (NSString *encoding in encodingsToAdd) { NSInteger parenthesisPosition = [encoding stringPosition:@" ("]; if (parenthesisPosition <= 0) { continue; } NSString *encodingPrefix = [encoding substringToIndex:parenthesisPosition]; if (ignoreFavored && [favoredEncodings containsObject:encodingPrefix]) { continue; } else if (ignoreFavored == NO && [favoredEncodings containsObject:encodingPrefix] == NO) { continue; } if ([encodingPrefix isEqualToString:encodingPrefixPrevious] == NO) { encodingPrefixPrevious = encodingPrefix; [self.primaryEncodingButton.menu addItem:[NSMenuItem separatorItem]]; [self.fallbackEncodingButton.menu addItem:[NSMenuItem separatorItem]]; } [self.primaryEncodingButton addItemWithTitle:encoding]; [self.fallbackEncodingButton addItemWithTitle:encoding]; } } #pragma mark - #pragma mark Initialization Handler - (void)start { [self startWithSelection:TDCServerPropertiesSheetSelectionDefault context:nil]; } - (void)startWithSelection:(TDCServerPropertiesSheetSelection)selection context:(nullable id)context { [self startSheet]; [self navigateToSelection:selection]; switch (selection) { case TDCServerPropertiesSheetSelectionNewIgnoreEntry: { if ([context isKindOfClass:[NSString class]]) { [self addIgnoreAddressBookEntryWithHostmask:context]; } else if ([context isKindOfClass:[IRCAddressBookEntry class]]) { [self editAddressBookEntryWithObject:context]; } break; } default: { break; } } } - (void)navigateToSelection:(TDCServerPropertiesSheetSelection)selection { if (selection == TDCServerPropertiesSheetSelectionDefault) { selection = TDCServerPropertiesSheetSelectionGeneral; } else if (selection == TDCServerPropertiesSheetSelectionNewIgnoreEntry) { selection = TDCServerPropertiesSheetSelectionAddressBook; } [self.navigationOutlineView navigateToItemWithIdentifier:selection]; [self.sheet recalculateKeyViewLoop]; } - (void)closeChildSheets { if (self.addressBookSheet) { [self.addressBookSheet close]; } else if (self.channelSheet) { [self.channelSheet close]; } else if (self.highlightSheet) { [self.highlightSheet close]; } else if (self.serverEndpointSheet) { [self.serverEndpointSheet close]; } else if (self.clientCertificateSelectCertificatePanel) { [NSApp stopModalWithCode:NSModalResponseCancel]; } } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } [self removeConfigurationDidChangeObserver]; [self closeChildSheets]; [self clearChannelListPredicate]; [self saveConfig]; if ([self.delegate respondsToSelector:@selector(serverPropertiesSheet:onOk:)]) { [self.delegate serverPropertiesSheet:self onOk:[self.config copy]]; } [super ok:nil]; } - (BOOL)okOrError { TDCServerPropertiesSheetSelection selection = self.navigationOutlineView.selectedItem.identifier; if ([self okOrErrorForSelection:selection] == NO) { return NO; } /* We want to show the error for the visible view first so that the user isn't jerked around. To accomplish this, we keep an array of views in which error occurs. Check error in visible view, remove that from array, then enumerate the rest. */ NSMutableArray *remainingSelections = [@[ @(TDCServerPropertiesSheetSelectionGeneral), @(TDCServerPropertiesSheetSelectionIdentity), @(TDCServerPropertiesSheetSelectionDisconnectMessages), @(TDCServerPropertiesSheetSelectionProxyServer), ] mutableCopy]; [remainingSelections removeObject:@(selection)]; for (NSNumber *selectionRemaining in remainingSelections) { if ([self okOrErrorForSelection:selectionRemaining.integerValue] == NO) { return NO; } } return YES; } - (BOOL)okOrErrorForSelection:(TDCServerPropertiesSheetSelection)selection { switch (selection) { case TDCServerPropertiesSheetSelectionGeneral: { if ([self okOrErrorForTextField:self.connectionNameTextField inSelection:selection] == NO) { return NO; } else if ([self okOrErrorForComboBox:self.serverAddressComboBox inSelection:selection] == NO) { return NO; } else if ([self okOrErrorForTextField:self.serverPortTextField inSelection:selection] == NO) { return NO; } break; } case TDCServerPropertiesSheetSelectionIdentity: { if ([self okOrErrorForTextField:self.nicknameTextField inSelection:selection] == NO) { return NO; } else if ([self okOrErrorForTextField:self.awayNicknameTextField inSelection:selection] == NO) { return NO; } else if ([self okOrErrorForTextField:self.alternateNicknamesTextField inSelection:selection] == NO) { return NO; } else if ([self okOrErrorForTextField:self.usernameTextField inSelection:selection] == NO) { return NO; } else if ([self okOrErrorForTextField:self.realNameTextField inSelection:selection] == NO) { return NO; } break; } case TDCServerPropertiesSheetSelectionDisconnectMessages: { if ([self okOrErrorForTextField:self.normalLeavingCommentTextField inSelection:selection] == NO) { return NO; } else if ([self okOrErrorForTextField:self.sleepModeQuitMessageTextField inSelection:selection] == NO) { return NO; } break; } case TDCServerPropertiesSheetSelectionProxyServer: { if ([self okOrErrorForTextField:self.proxyAddressTextField inSelection:selection] == NO) { return NO; } else if ([self okOrErrorForTextField:self.proxyPortTextField inSelection:selection] == NO) { return NO; } break; } default: { break; } } // switch() return YES; } - (BOOL)okOrErrorForComboBox:(TVCValidatedComboBox *)comboBox inSelection:(TDCServerPropertiesSheetSelection)selection { if (comboBox.valueIsValid) { return YES; } [self navigateToSelection:selection]; XRPerformBlockAsynchronouslyOnMainQueue(^{ [comboBox showValidationErrorPopover]; }); return NO; } - (BOOL)okOrErrorForTextField:(TVCValidatedTextField *)textField inSelection:(TDCServerPropertiesSheetSelection)selection { if (textField.valueIsValid) { return YES; } [self navigateToSelection:selection]; /* Give navigation time to settle before trying to attach popover */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [textField showValidationErrorPopover]; }); return NO; } - (void)cancel:(id)sender { [self removeConfigurationDidChangeObserver]; [self closeChildSheets]; [super cancel:nil]; } - (void)addConfigurationDidChangeObserver { if (self.client == nil) { return; } [RZNotificationCenter() addObserver:self selector:@selector(underlyingConfigurationChanged:) name:IRCClientConfigurationWasUpdatedNotification object:self.client]; } - (void)removeConfigurationDidChangeObserver { [RZNotificationCenter() removeObserver:self]; } - (void)underlyingConfigurationChanged:(NSNotification *)notification { IRCClient *client = notification.object; NSWindow *window = self.sheet.deepestWindow; [TDCAlert alertSheetWithWindow:window body:TXTLS(@"TDCServerPropertiesSheet[oz4-kb]") title:TXTLS(@"TDCServerPropertiesSheet[bzh-il]") defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]") otherButton:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { if (buttonClicked != TDCAlertResponseDefault) { return; } [self close]; [client updateStoredConfiguration]; self.config = [client.config mutableCopy]; [self loadConfig]; [self start]; }]; } - (void)loadConfig { /* General */ self.connectionNameTextField.stringValue = self.config.connectionName; self.autoConnectCheck.state = self.config.autoConnect; self.autoReconnectCheck.state = self.config.autoReconnect; self.autoDisconnectOnSleepCheck.state = self.config.autoSleepModeDisconnect; /* ZNC Bouncer */ self.zncIgnoreConfiguredAutojoinCheck.state = self.config.zncIgnoreConfiguredAutojoin; self.zncIgnorePlaybackNotificationsCheck.state = self.config.zncIgnorePlaybackNotifications; self.zncOnlyPlaybackLatestCheck.state = self.config.zncOnlyPlaybackLatest; /* Network Socket */ IRCConnectionAddressType addressType = self.config.addressType; self.connectionIPv4AddressTypeCheck.state = (addressType == IRCConnectionAddressTypeIPv4); self.connectionIPv6AddressTypeCheck.state = (addressType == IRCConnectionAddressTypeIPv6); self.connectionDefaultAddressTypeCheck.state = (addressType == IRCConnectionAddressTypeDefault); self.pongTimerCheck.state = self.config.performPongTimer; self.performDisconnectOnPongTimerCheck.state = self.config.performDisconnectOnPongTimer; self.disconnectOnReachabilityChangeCheck.state = self.config.performDisconnectOnReachabilityChange; self.validateServerCertificateChainCheck.state = self.config.validateServerCertificateChain; [self.preferredCipherSuitesButton selectItemWithTag:self.config.cipherSuites]; /* Identity */ if (self.config.nickname.length > 0) { self.nicknameTextField.stringValue = self.config.nickname; } else { self.nicknameTextField.stringValue = [TPCPreferences defaultNickname]; } self.awayNicknameTextField.stringValue = self.config.awayNickname; NSString *alternateNicknamesString = [self.config.alternateNicknames componentsJoinedByString:@" "]; self.alternateNicknamesTextField.stringValue = alternateNicknamesString; if (self.config.username.length > 0) { self.usernameTextField.stringValue = self.config.username; } else { self.usernameTextField.stringValue = [TPCPreferences defaultUsername]; } if (self.config.realName.length > 0) { self.realNameTextField.stringValue = self.config.realName; } else { self.realNameTextField.stringValue = [TPCPreferences defaultRealName]; } self.nicknamePasswordTextField.stringValue = self.config.nicknamePassword; self.autojoinWaitsForNickServCheck.state = self.config.autojoinWaitsForNickServ; self.hideAutojoinDelayedWarningsCheck.state = (self.config.hideAutojoinDelayedWarnings == NO); /* Messages */ self.normalLeavingCommentTextField.stringValue = self.config.normalLeavingComment; self.sleepModeQuitMessageTextField.stringValue = self.config.sleepModeLeavingComment; /* Encoding */ NSString *primaryEncodingTitle = [self.encodingList firstKeyForObject:@(self.config.primaryEncoding)]; [self.primaryEncodingButton selectItemWithTitle:primaryEncodingTitle]; NSString *fallbackEncodingTitle = [self.encodingList firstKeyForObject:@(self.config.fallbackEncoding)]; [self.fallbackEncodingButton selectItemWithTitle:fallbackEncodingTitle]; /* Proxy Server */ [self.proxyTypeButton selectItemWithTag:self.config.proxyType]; self.proxyAddressTextField.stringValue = self.config.proxyAddress; self.proxyPortTextField.integerValue = self.config.proxyPort; self.proxyUsernameTextField.stringValue = self.config.proxyUsername; self.proxyPasswordTextField.stringValue = self.config.proxyPassword; /* Connect Commands */ NSString *loginCommandsString = [self.config.loginCommands componentsJoinedByString:@"\n"]; self.connectCommandsField.string = loginCommandsString; self.setInvisibleModeOnConnectCheck.state = self.config.setInvisibleModeOnConnect; /* Flood Control */ self.floodControlDelayTimerSliderTempValue = self.config.floodControlDelayTimerInterval; self.floodControlMessageCountSliderTempValue = self.config.floodControlMaximumMessages; /* Mutable Stores */ [self.addressBookArrayController addObjects:self.config.ignoreList]; [self.channelListArrayController addObjects:self.config.channelList]; [self.highlightListArrayController addObjects:self.config.highlightList]; [self.serverListArrayController addObjects:self.config.serverList]; /* Special loads */ [self loadPrimaryServerEndpoint]; [self loadConfigPostflight]; } - (void)loadPrimaryServerEndpoint { IRCServer *server = self.serverList.firstObject; if (server == nil) { self.serverAddressComboBox.stringValue = @""; self.serverPortTextField.integerValue = IRCConnectionDefaultServerPort; self.prefersSecuredConnectionCheck.state = NSControlStateValueOff; self.serverPasswordTextField.stringValue = @""; return; } self.populatingPrimaryServer = YES; NSString *serverAddress = server.serverAddress; if (serverAddress) { IRCNetwork *serverAddressNetwork = [self.networkList networkWithServerAddress:serverAddress]; if (serverAddressNetwork) { self.serverAddressComboBox.stringValue = serverAddressNetwork.networkName; } else { self.serverAddressComboBox.stringValue = serverAddress; } } self.serverPortTextField.integerValue = server.serverPort; self.prefersSecuredConnectionCheck.state = server.prefersSecuredConnection; self.serverPasswordTextField.stringValue = server.serverPassword; self.populatingPrimaryServer = NO; } - (void)loadConfigPostflight { [self setChannelListPredicate]; [self updateAddressBookPage]; [self updateChannelListPage]; [self updateClientCertificatePage]; [self updateHighlightsPage]; [self updateIdentityPage]; [self preferredCipherSuitesChanged:nil]; [self proxyTypeChanged:nil]; } - (void)saveConfig { /* General */ self.config.connectionName = self.connectionNameTextField.value; self.config.autoConnect = (self.autoConnectCheck.state == NSControlStateValueOn); self.config.autoReconnect = (self.autoReconnectCheck.state == NSControlStateValueOn); self.config.autoSleepModeDisconnect = (self.autoDisconnectOnSleepCheck.state == NSControlStateValueOn); /* ZNC Bouncer */ self.config.zncIgnoreConfiguredAutojoin = (self.zncIgnoreConfiguredAutojoinCheck.state == NSControlStateValueOn); self.config.zncIgnorePlaybackNotifications = (self.zncIgnorePlaybackNotificationsCheck.state == NSControlStateValueOn); self.config.zncOnlyPlaybackLatest = (self.zncOnlyPlaybackLatestCheck.state == NSControlStateValueOn); /* Network Socket */ self.config.performPongTimer = (self.pongTimerCheck.state == NSControlStateValueOn); self.config.performDisconnectOnPongTimer = (self.performDisconnectOnPongTimerCheck.state == NSControlStateValueOn); self.config.performDisconnectOnReachabilityChange = (self.disconnectOnReachabilityChangeCheck.state == NSControlStateValueOn); self.config.validateServerCertificateChain = (self.validateServerCertificateChainCheck.state == NSControlStateValueOn); self.config.cipherSuites = self.preferredCipherSuitesButton.selectedTag; /* Identity */ self.config.nickname = self.nicknameTextField.value; self.config.username = self.usernameTextField.value; self.config.realName = self.realNameTextField.value; self.config.awayNickname = self.awayNicknameTextField.value; self.config.nicknamePassword = self.nicknamePasswordTextField.trimmedStringValue; self.config.autojoinWaitsForNickServ = (self.autojoinWaitsForNickServCheck.state == NSControlStateValueOn); self.config.hideAutojoinDelayedWarnings = (self.hideAutojoinDelayedWarningsCheck.state != NSControlStateValueOn); /* Alternate nicknames */ NSString *alternateNicknamesString = self.alternateNicknamesTextField.value; NSArray *alternateNicknames = [alternateNicknamesString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; self.config.alternateNicknames = [alternateNicknames arrayByRemovingEmptyValuesAndUniquing]; /* Messages */ self.config.sleepModeLeavingComment = self.sleepModeQuitMessageTextField.value; self.config.normalLeavingComment = self.normalLeavingCommentTextField.value; /* Encoding */ NSStringEncoding primaryEncoding = [self.encodingList unsignedIntegerForKey:self.primaryEncodingButton.title]; self.config.primaryEncoding = primaryEncoding; NSStringEncoding fallbackEncoding = [self.encodingList unsignedIntegerForKey:self.fallbackEncodingButton.title]; self.config.fallbackEncoding = fallbackEncoding; /* Proxy Server */ self.config.proxyType = self.proxyTypeButton.selectedTag; self.config.proxyAddress = self.proxyAddressTextField.lowercaseValue; self.config.proxyPort = self.proxyPortTextField.integerValue; self.config.proxyUsername = self.proxyUsernameTextField.trimmedFirstTokenStringValue; self.config.proxyPassword = self.proxyPasswordTextField.trimmedStringValue; /* Connect Commands */ NSString *connectCommandsString = self.connectCommandsField.string; NSArray *connectCommands = [connectCommandsString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; self.config.loginCommands = [connectCommands arrayByRemovingEmptyValues:YES trimming:YES uniquing:NO]; self.config.setInvisibleModeOnConnect = (self.setInvisibleModeOnConnectCheck.state == NSControlStateValueOn); /* Flood Control */ self.config.floodControlMaximumMessages = self.floodControlMessageCountSlider.integerValue; self.config.floodControlDelayTimerInterval = self.floodControlDelayTimerSlider.integerValue; /* Mutable stores. */ self.config.channelList = self.channelListArrayController.arrangedObjects; self.config.highlightList = self.highlightListArrayController.arrangedObjects; self.config.ignoreList = self.addressBookArrayController.arrangedObjects; self.config.serverList = self.serverListArrayController.arrangedObjects; } - (void)restorePreviousValuesForPrimaryServer { IRCServer *previousServer = self.previousPrimaryServer; if (previousServer == nil) { return; } self.populatingPrimaryServer = YES; self.serverPortTextField.integerValue = previousServer.serverPort; self.prefersSecuredConnectionCheck.state = previousServer.prefersSecuredConnection; self.previousPrimaryServer = nil; self.populatingPrimaryServer = NO; } - (void)saveCurrentValuesForPrimaryServer { /* Do not change the previous primary server between predefined servers. */ if (self.previousPrimaryServer != nil) { return; } IRCServer *server = self.serverList.firstObject; if (server) { self.previousPrimaryServer = server; return; } IRCServerMutable *serverMutable = [IRCServerMutable new]; serverMutable.serverPort = self.serverPortTextField.integerValue; serverMutable.prefersSecuredConnection = (self.prefersSecuredConnectionCheck.state == NSControlStateValueOn); self.previousPrimaryServer = serverMutable; } - (void)populateDefaultsForPreconfiguredNetwork { if (self.populatingPrimaryServer) { return; } NSString *serverAddress = self.serverAddressComboBox.value; if ([serverAddress isEqualToString:self.lastServerAddressValue]) { return; } self.lastServerAddressValue = serverAddress; IRCNetwork *network = [self.networkList networkNamed:serverAddress]; if (network == nil) { network = [self.networkList networkWithServerAddress:serverAddress]; } if (network == nil) { [self restorePreviousValuesForPrimaryServer]; return; } self.populatingPrimaryServer = YES; /* The predefined list in the combo box is only the network name. IRCNetwork is also capable of matching known server addresses. If we have a known network but it's not a predefined value, then it very might be a server address. */ /* Let's replace the given input that matches a network with the network name itself. */ if (self.serverAddressComboBox.valueIsPredefined == NO) { NSString *networkName = network.networkName; self.serverAddressComboBox.stringValue = networkName; self.lastServerAddressValue = networkName; } /* Populate other defaults */ [self saveCurrentValuesForPrimaryServer]; self.serverPortTextField.integerValue = network.serverPort; self.prefersSecuredConnectionCheck.state = (network.prefersSecuredConnection == NSControlStateValueOn); self.populatingPrimaryServer = NO; } - (void)updateChannelListPage { NSInteger selectedRow = self.channelListTable.selectedRow; self.deleteChannelButton.enabled = (selectedRow >= 0); self.editChannelButton.enabled = (selectedRow >= 0); } - (void)clearChannelListPredicate { self.channelListArrayController.filterPredicate = nil; } - (void)setChannelListPredicate { self.channelListArrayController.filterPredicate = [NSPredicate predicateWithFormat:@"type == 0"]; // Is channel type } - (void)updateAddressBookPage { NSInteger selectedRow = self.addressBookTable.selectedRow; self.deleteAddressBookEntryButton.enabled = (selectedRow >= 0); self.editAddressBookEntryButton.enabled = (selectedRow >= 0); } - (void)updateHighlightsPage { NSInteger selectedRow = self.highlightsTable.selectedRow; self.deleteHighlightButton.enabled = (selectedRow >= 0); self.editHighlightButton.enabled = (selectedRow >= 0); } - (void)useSSLCheckChanged:(id)sender { NSInteger serverPort = self.serverPortTextField.integerValue; BOOL useSSL = (self.prefersSecuredConnectionCheck.state == NSControlStateValueOn); if (useSSL) { if (serverPort == 6667) { self.serverPortTextField.stringValue = TXLocalizationNotNeeded(@"6697"); } } else { if (serverPort == 6697) { self.serverPortTextField.stringValue = TXLocalizationNotNeeded(@"6667"); } } [self rebuildMutableServerEndpointListIfNeeded:sender]; } - (void)updateIdentityPage { self.hideAutojoinDelayedWarningsCheck.hidden = (self.autojoinWaitsForNickServCheck.state == NSControlStateValueOff); } #pragma mark - #pragma mark Properties - (NSArray *)addressBookList { return self.addressBookArrayController.arrangedObjects; } - (NSArray *)channelList { return self.channelListArrayController.arrangedObjects; } - (NSArray *)highlightList { return self.highlightListArrayController.arrangedObjects; } - (NSArray *)serverList { return self.serverListArrayController.arrangedObjects; } #pragma mark - #pragma mark Delegates - (void)controlTextDidChange:(NSNotification *)notification { NSControl *sender = notification.object; if (sender == self.serverPasswordTextField) { [self rebuildMutableServerEndpointListIfNeeded:sender]; } } - (void)validatedTextFieldTextDidChange:(id)sender { if (sender == self.serverAddressComboBox) { [self populateDefaultsForPreconfiguredNetwork]; [self rebuildMutableServerEndpointListIfNeeded:sender]; } else if (sender == self.serverPortTextField) { [self rebuildMutableServerEndpointListIfNeeded:sender]; } } #pragma mark - #pragma mark Actions - (void)autojoinWaitsForNickServChanged:(id)sender { [self updateIdentityPage]; if (self.autojoinWaitsForNickServCheck.state != NSControlStateValueOn) { return; } if (self.nicknamePasswordTextField.stringValue.length > 0 || self.clientCertificateResetCertificateButton.enabled) { return; } [TDCAlert modalAlertWithMessage:TXTLS(@"TDCServerPropertiesSheet[26u-j8]") title:TXTLS(@"TDCServerPropertiesSheet[94r-eq]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } - (void)preferredCipherSuitesChanged:(id)sender { NSInteger cipherSuites = self.preferredCipherSuitesButton.selectedTag; self.viewListOfPreferredCipherSuitesButton.enabled = (cipherSuites != RCMCipherSuiteCollectionNone); } - (void)preferredCipherSuitesViewList:(id)sender { NSInteger cipherSuites = self.preferredCipherSuitesButton.selectedTag; NSArray *cipherSuitesDescriptions = [RCMSecureTransport descriptionsForCipherListCollection:cipherSuites withProtocol:YES]; NSString *cipherSuitesDescription = [cipherSuitesDescriptions componentsJoinedByString:@"\n"]; NSString *cipherSuitesTitle = self.preferredCipherSuitesButton.titleOfSelectedItem; [TDCAlert alertSheetWithWindow:self.sheet body:TXTLS(@"TDCServerPropertiesSheet[k50-8n]", cipherSuitesDescription) title:TXTLS(@"TDCServerPropertiesSheet[yko-5g]", cipherSuitesTitle) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil otherButton:nil]; } - (void)proxyTypeChanged:(id)sender { NSInteger proxyType = self.proxyTypeButton.selectedTag; BOOL isSystemSocksProxyEnabled = (proxyType == IRCConnectionProxyTypeAutomatic); BOOL isTorBrowserProxyEnabled = (proxyType == IRCConnectionProxyTypeTor); BOOL supportsAuthentication = (proxyType == IRCConnectionProxyTypeSocks5); BOOL httpsEnabled = (proxyType == IRCConnectionProxyTypeHTTP || proxyType == IRCConnectionProxyTypeHTTPS); BOOL socksEnabled = (proxyType == IRCConnectionProxyTypeSocks4 || proxyType == IRCConnectionProxyTypeSocks5); BOOL enabled = (httpsEnabled || socksEnabled); self.contentViewProxyServerInputView.hidden = (httpsEnabled == NO && socksEnabled == NO); self.contentViewProxyServerTorBrowserView.hidden = (isTorBrowserProxyEnabled == NO); self.contentViewProxyServerSystemSocksView.hidden = (isSystemSocksProxyEnabled == NO); self.proxyAddressTextField.enabled = enabled; self.proxyPortTextField.enabled = enabled; self.proxyUsernameTextField.enabled = (socksEnabled && supportsAuthentication); self.proxyPasswordTextField.enabled = (socksEnabled && supportsAuthentication); [self.proxyAddressTextField performValidation]; [self.proxyPortTextField performValidation]; } - (void)openProxySettingsInSystemPreferences:(id)sender { [TDCPreferencesController openProxySettingsInSystemPreferences]; } - (void)toggleAdvancedEncodings:(id)sender { NSString *primaryEncoding = self.primaryEncodingButton.titleOfSelectedItem; NSString *fallbackEncoding = self.fallbackEncodingButton.titleOfSelectedItem; [self populateEncodings]; NSMenuItem *primaryEncodingItem = nil; if (primaryEncoding) { primaryEncodingItem = [self.primaryEncodingButton itemWithTitle:primaryEncoding]; } if (primaryEncodingItem == nil) { primaryEncoding = [NSString localizedNameOfStringEncoding:TXDefaultPrimaryStringEncoding]; } NSMenuItem *fallbackEncodingItem = nil; if (fallbackEncoding) { fallbackEncodingItem = [self.fallbackEncodingButton itemWithTitle:fallbackEncoding]; } if (fallbackEncodingItem == nil) { fallbackEncoding = [NSString localizedNameOfStringEncoding:TXDefaultFallbackStringEncoding]; } [self.primaryEncodingButton selectItemWithTitle:primaryEncoding]; [self.fallbackEncodingButton selectItemWithTitle:fallbackEncoding]; } - (void)preferredInternetProtocolChanged:(id)sender { TEXTUAL_IGNORE_DEPRECATION_BEGIN /* Changing the property triggers a deprecation log to console which is just unnecessary output when each time we change it. */ if (self.config.connectionPrefersIPv4) { self.config.connectionPrefersIPv4 = NO; } TEXTUAL_IGNORE_DEPRECATION_END self.config.addressType = [sender tag]; } #pragma mark - #pragma mark SSL Certificate - (void)onClientCertificateFingerprintSHA512CopyRequested:(id)sender { NSString *fingerprint = self.clientCertificateSHA512FingerprintField.stringValue; NSString *command = [NSString stringWithFormat:@"/msg NickServ cert add %@", fingerprint]; RZPasteboard().stringContent = command; } - (void)onClientCertificateFingerprintSHA2CopyRequested:(id)sender { NSString *fingerprint = self.clientCertificateSHA2FingerprintField.stringValue; NSString *command = [NSString stringWithFormat:@"/msg NickServ cert add %@", fingerprint]; RZPasteboard().stringContent = command; } - (void)onClientCertificateFingerprintSHA1CopyRequested:(id)sender { NSString *fingerprint = self.clientCertificateSHA1FingerprintField.stringValue; NSString *command = [NSString stringWithFormat:@"/msg NickServ cert add %@", fingerprint]; RZPasteboard().stringContent = command; } - (void)onClientCertificateFingerprintMD5CopyRequested:(id)sender { NSString *fingerprint = self.clientCertificateMD5FingerprintField.stringValue; NSString *command = [NSString stringWithFormat:@"/msg NickServ cert add %@", fingerprint]; RZPasteboard().stringContent = command; } - (void)readClientCertificateCommonName:(NSString **)commonNameOut sha512Fingerprint:(NSString **)sha512FingerprintOut sha2Fingerprint:(NSString **)sha2FingerprintOut sha1Fingerprint:(NSString **)sha1FingerprintOut md5Fingerprint:(NSString **)md5FingerprintOut { NSData *certificateDataIn = self.config.identityClientSideCertificate; if (certificateDataIn == nil) { return; } /* ====================================== */ SecKeychainItemRef certificateRef; CFDataRef certificateDataInRef = (__bridge CFDataRef)certificateDataIn; OSStatus status = SecKeychainItemCopyFromPersistentReference(certificateDataInRef, &certificateRef); if (status != noErr) { return; } /* ====================================== */ CFStringRef commonNameRef; status = SecCertificateCopyCommonName((SecCertificateRef)certificateRef, &commonNameRef); if (status != noErr) { CFRelease(certificateRef); return; } *commonNameOut = (__bridge NSString *)(commonNameRef); CFRelease(commonNameRef); /* ====================================== */ CFDataRef certificateDataRef = SecCertificateCopyData((SecCertificateRef)certificateRef); if (certificateDataRef) { NSData *certificateData = (__bridge NSData *)certificateDataRef; *sha512FingerprintOut = certificateData.sha512; *sha2FingerprintOut = certificateData.sha256; *sha1FingerprintOut = certificateData.sha1; *md5FingerprintOut = certificateData.md5; CFRelease(certificateDataRef); } /* ====================================== */ CFRelease(certificateRef); } - (void)saveClientCertificateWithIdentity:(SecIdentityRef)identityInRef { if (identityInRef == NULL) { return; } /* ====================================== */ SecCertificateRef certificateRef; OSStatus status = SecIdentityCopyCertificate(identityInRef, &certificateRef); if (status != noErr) { LogToConsoleError("Operation Failed (2): %{public}i", status); return; } /* ====================================== */ CFDataRef certificateDataRef; status = SecKeychainItemCreatePersistentReference((SecKeychainItemRef)certificateRef, &certificateDataRef); if (status != noErr) { CFRelease(certificateRef); LogToConsoleError("Operation Failed (3): %{public}i", status); return; } /* ====================================== */ self.config.identityClientSideCertificate = (__bridge NSData *)certificateDataRef; if (self.prefersSecuredConnectionCheck.state == NSControlStateValueOff) { self.prefersSecuredConnectionCheck.state = NSControlStateValueOn; [self useSSLCheckChanged:nil]; } /* ====================================== */ CFRelease(certificateRef); CFRelease(certificateDataRef); } - (void)updateClientCertificatePage { NSString *commonName = nil; NSString *sha512Fingerprint = nil; NSString *sha2Fingerprint = nil; NSString *sha1Fingerprint = nil; NSString *md5Fingerprint = nil; [self readClientCertificateCommonName:&commonName sha512Fingerprint:&sha512Fingerprint sha2Fingerprint:&sha2Fingerprint sha1Fingerprint:&sha1Fingerprint md5Fingerprint:&md5Fingerprint]; BOOL hasNoCertificate = (commonName.length == 0); if (hasNoCertificate) { self.clientCertificateCommonNameField.stringValue = TXTLS(@"TDCServerPropertiesSheet[6xz-ec]"); self.clientCertificateSHA512FingerprintField.stringValue = TXTLS(@"TDCServerPropertiesSheet[6xz-ec]"); self.clientCertificateSHA2FingerprintField.stringValue = TXTLS(@"TDCServerPropertiesSheet[6xz-ec]"); self.clientCertificateSHA1FingerprintField.stringValue = TXTLS(@"TDCServerPropertiesSheet[6xz-ec]"); self.clientCertificateMD5FingerprintField.stringValue = TXTLS(@"TDCServerPropertiesSheet[6xz-ec]"); } else { self.clientCertificateCommonNameField.stringValue = commonName; self.clientCertificateSHA512FingerprintField.stringValue = sha512Fingerprint.uppercaseString; self.clientCertificateSHA2FingerprintField.stringValue = sha2Fingerprint.uppercaseString; self.clientCertificateSHA1FingerprintField.stringValue = sha1Fingerprint.uppercaseString; self.clientCertificateMD5FingerprintField.stringValue = md5Fingerprint.uppercaseString; } self.clientCertificateResetCertificateButton.enabled = (hasNoCertificate == NO); self.clientCertificateSHA512FingerprintCopyButton.enabled = (hasNoCertificate == NO); self.clientCertificateSHA2FingerprintCopyButton.enabled = (hasNoCertificate == NO); self.clientCertificateSHA1FingerprintCopyButton.enabled = (hasNoCertificate == NO); self.clientCertificateMD5FingerprintCopyButton.enabled = (hasNoCertificate == NO); } - (void)onClientCertificateResetRequested:(id)sender { self.config.identityClientSideCertificate = nil; [self updateClientCertificatePage]; } - (void)onClientCertificateChangeRequested:(id)sender { CFArrayRef identities = NULL; NSDictionary *queryFlags = @{ (id)kSecClass : (id)kSecClassIdentity, (id)kSecMatchLimit : (id)kSecMatchLimitAll, (id)kSecReturnRef : (id)kCFBooleanTrue }; OSStatus queryStatus = SecItemCopyMatching((__bridge CFDictionaryRef)queryFlags, (CFTypeRef *)&identities); if (queryStatus != noErr) { LogToConsoleError("Operation Failed (1): %{public}i", queryStatus); } if (identities == NULL || CFArrayGetCount(identities) == 0) { [TDCAlert alertWithMessage:TXTLS(@"TDCServerPropertiesSheet[489-hG]") title:TXTLS(@"TDCServerPropertiesSheet[pmk-os]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil otherButton:TXTLS(@"TDCServerPropertiesSheet[3ju-lo]") completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { if (buttonClicked == TDCAlertResponseOther) { [TLOpenLink openWithString:@"https://help.codeux.com/textual/Using-CertFP.kb" inBackground:NO]; } }]; return; } SFChooseIdentityPanel *panel = [SFChooseIdentityPanel sharedChooseIdentityPanel]; self.clientCertificateSelectCertificatePanel = panel; [panel setInformativeText:TXTLS(@"TDCServerPropertiesSheet[mi4-fd]")]; [panel setAlternateButtonTitle:TXTLS(@"Prompts[qso-2g]")]; NSInteger panelResponse = [panel runModalForIdentities:(__bridge NSArray *)(identities) message:TXTLS(@"TDCServerPropertiesSheet[6wq-i4]")]; CFRelease(identities); if (panelResponse == NSModalResponseOK) { SecIdentityRef identity = [panel identity]; [self saveClientCertificateWithIdentity:identity]; [self updateClientCertificatePage]; } self.clientCertificateSelectCertificatePanel = nil; } #pragma mark - #pragma mark Redundancy - (void)editSeverEndpoints:(id)sender { TDCServerEndpointListSheet *sheet = [[TDCServerEndpointListSheet alloc] initWithWindow:self.sheet]; sheet.delegate = self; [sheet startWithServerList:self.serverList]; self.serverEndpointSheet = sheet; } - (void)serverEndpointListSheet:(TDCServerEndpointListSheet *)ender onOk:(NSArray *)serverList { [self.serverListArrayController removeAllArrangedObjects]; [self.serverListArrayController addObjects:serverList]; [self loadPrimaryServerEndpoint]; } - (void)serverEndpointListSheetWillClose:(TDCServerEndpointListSheet *)sender { self.serverEndpointSheet = nil; } - (void)rebuildMutableServerEndpointList:(id)sender { NSParameterAssert(sender != nil); if (self.populatingPrimaryServer) { return; } IRCServer *server = self.serverList.firstObject; IRCServerMutable *serverMutable = nil; if (server == nil) { serverMutable = [IRCServerMutable new]; } else { serverMutable = [server mutableCopy]; } { NSString *serverAddress = self.serverAddressComboBox.value; IRCNetwork *serverAddressNetwork = [self.networkList networkNamed:serverAddress]; if (serverAddressNetwork) { serverMutable.serverAddress = serverAddressNetwork.serverAddress; } else { serverMutable.serverAddress = serverAddress.lowercaseString; } } serverMutable.serverPort = self.serverPortTextField.integerValue; serverMutable.prefersSecuredConnection = (self.prefersSecuredConnectionCheck.state == NSControlStateValueOn); serverMutable.serverPassword = self.serverPasswordTextField.trimmedStringValue; server = [serverMutable copy]; if (self.serverList.count == 0) { [self.serverListArrayController addObject:server]; } else { [self.serverListArrayController replaceObjectAtArrangedObjectIndex:0 withObject:server]; } } - (void)rebuildMutableServerEndpointListIfNeeded:(id)sender { NSParameterAssert(sender != nil); /* We only want to rebuild the IRCServer at index 0 when the user changes it. This method is invoked whenever something changes related to that server. If it was not changed while the server endpoint sheet was open, then we must update the mutable server list to include the change. */ if (self.serverEndpointSheet != nil) { return; } [self rebuildMutableServerEndpointList:sender]; } #pragma mark - #pragma mark Highlight Actions - (void)addHighlight:(id)sender { TDCHighlightEntrySheet *sheet = [[TDCHighlightEntrySheet alloc] initWithConfig:nil]; sheet.delegate = self; sheet.window = self.sheet; [sheet startWithChannels:self.channelList]; self.highlightSheet = sheet; } - (void)editHighlight:(id)sender { NSInteger selectedRow = self.highlightsTable.selectedRow; if (selectedRow < 0) { return; } IRCHighlightMatchCondition *config = self.highlightList[selectedRow]; TDCHighlightEntrySheet *sheet = [[TDCHighlightEntrySheet alloc] initWithConfig:config]; sheet.delegate = self; sheet.window = self.sheet; [sheet startWithChannels:self.channelList]; self.highlightSheet = sheet; } - (void)highlightEntrySheet:(TDCHighlightEntrySheet *)sender onOk:(IRCHighlightMatchCondition *)config { NSUInteger entryIndex = [self.highlightList indexOfObjectPassingTest:^BOOL(id object, NSUInteger index, BOOL *stop) { if ([[object uniqueIdentifier] isEqualToString:config.uniqueIdentifier]) { return YES; } else { return NO; } }]; if (entryIndex == NSNotFound) { [self.highlightListArrayController addObject:config]; } else { [self.highlightListArrayController replaceObjectAtArrangedObjectIndex:entryIndex withObject:config]; } } - (void)highlightEntrySheetWillClose:(TDCHighlightEntrySheet *)sender { self.highlightSheet = nil; } - (void)deleteHighlight:(id)sender { NSInteger selectedRow = self.highlightsTable.selectedRow; if (selectedRow < 0) { return; } [self.highlightListArrayController removeObjectAtArrangedObjectIndex:selectedRow]; NSUInteger listCount = self.highlightList.count; if (listCount > 0) { if (listCount <= selectedRow) { [self.highlightsTable selectItemAtIndex:(listCount - 1)]; } else { [self.highlightsTable selectItemAtIndex:selectedRow]; } } } #pragma mark - #pragma mark Channel Actions - (void)addChannel:(id)sender { TDCChannelPropertiesSheet *sheet = [[TDCChannelPropertiesSheet alloc] initWithWindow:self.sheet]; sheet.delegate = self; [sheet start]; self.channelSheet = sheet; } - (void)editChannel:(id)sender { NSInteger selectedRow = self.channelListTable.selectedRow; if (selectedRow < 0) { return; } IRCChannelConfig *config = self.channelList[selectedRow]; TDCChannelPropertiesSheet *sheet = [[TDCChannelPropertiesSheet alloc] initWithConfig:config]; sheet.delegate = self; sheet.window = self.sheet; [sheet start]; self.channelSheet = sheet; } - (void)channelPropertiesSheet:(TDCChannelPropertiesSheet *)sender onOk:(IRCChannelConfig *)config { NSUInteger entryIndex = [self.channelList indexOfObjectPassingTest:^BOOL(id object, NSUInteger index, BOOL *stop) { if ([[object uniqueIdentifier] isEqualToString:config.uniqueIdentifier]) { return YES; } else { return NO; } }]; [self clearChannelListPredicate]; if (entryIndex == NSNotFound) { [self.channelListArrayController addObject:config]; } else { [self.channelListArrayController replaceObjectAtArrangedObjectIndex:entryIndex withObject:config]; } [self setChannelListPredicate]; } - (void)channelPropertiesSheetWillClose:(TDCChannelPropertiesSheet *)sender { self.channelSheet = nil; } - (void)deleteChannel:(id)sender { NSInteger selectedRow = self.channelListTable.selectedRow; if (selectedRow < 0) { return; } [self clearChannelListPredicate]; [self.channelListArrayController removeObjectAtArrangedObjectIndex:selectedRow]; [self setChannelListPredicate]; NSUInteger listCount = self.channelList.count; if (listCount > 0) { if (listCount <= selectedRow) { [self.channelListTable selectItemAtIndex:(listCount - 1)]; } else { [self.channelListTable selectItemAtIndex:selectedRow]; } } } #pragma mark - #pragma mark Address Book Actions - (void)showAddAddressBookEntryMenu:(id)sender { [self.addAddressBookEntryMenu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0, 0) inView:sender]; } - (void)addIgnoreAddressBookEntry { [self addIgnoreAddressBookEntryWithHostmask:nil]; } - (void)addIgnoreAddressBookEntryWithHostmask:(nullable NSString *)hostmask { TDCAddressBookSheet *sheet = nil; if (hostmask) { IRCAddressBookEntry *config = [IRCAddressBookEntry newIgnoreEntryForHostmask:hostmask]; sheet = [[TDCAddressBookSheet alloc] initWithConfig:config]; } else { sheet = [[TDCAddressBookSheet alloc] initWithEntryType:IRCAddressBookEntryTypeIgnore]; } sheet.delegate = self; sheet.window = self.sheet; [sheet start]; self.addressBookSheet = sheet; } - (void)addUserTrackingAddressBookEntry { TDCAddressBookSheet *sheet = [[TDCAddressBookSheet alloc] initWithEntryType:IRCAddressBookEntryTypeUserTracking]; sheet.delegate = self; sheet.window = self.sheet; [sheet start]; self.addressBookSheet = sheet; } - (void)addAddressBookEntry:(id)sender { if ([sender tag] == 3) { [self addIgnoreAddressBookEntry]; } else if ([sender tag] == 4) { [self addUserTrackingAddressBookEntry]; } } - (void)editAddressBookEntryWithObject:(IRCAddressBookEntry *)entryObject { NSInteger tableIndex = [self.addressBookList indexOfObject:entryObject]; if (tableIndex == NSNotFound) { return; } [self.addressBookTable selectItemAtIndex:tableIndex]; [self editAddressBookEntry:nil]; } - (void)editAddressBookEntry:(id)sender { NSInteger selectedRow = self.addressBookTable.selectedRow; if (selectedRow < 0) { return; } IRCAddressBookEntry *config = self.addressBookList[selectedRow]; TDCAddressBookSheet *sheet = [[TDCAddressBookSheet alloc] initWithConfig:config]; sheet.delegate = self; sheet.window = self.sheet; [sheet start]; self.addressBookSheet = sheet; } - (void)addressBookSheet:(TDCAddressBookSheet *)sender onOk:(IRCAddressBookEntry *)config { NSUInteger entryIndex = [self.addressBookList indexOfObjectPassingTest:^BOOL(id object, NSUInteger index, BOOL *stop) { if ([[object uniqueIdentifier] isEqualToString:config.uniqueIdentifier]) { return YES; } else { return NO; } }]; if (entryIndex == NSNotFound) { [self.addressBookArrayController addObject:config]; } else { [self.addressBookArrayController replaceObjectAtArrangedObjectIndex:entryIndex withObject:config]; } } - (void)addressBookSheetWillClose:(TDCAddressBookSheet *)sender { self.addressBookSheet = nil; } - (void)deleteAddressBookEntry:(id)sender { NSInteger selectedRow = self.addressBookTable.selectedRow; if (selectedRow < 0) { return; } [self.addressBookArrayController removeObjectAtArrangedObjectIndex:selectedRow]; NSUInteger listCount = self.addressBookList.count; if (listCount > 0) { if (listCount <= selectedRow) { [self.addressBookTable selectItemAtIndex:(listCount - 1)]; } else { [self.addressBookTable selectItemAtIndex:selectedRow]; } } } #pragma mark - #pragma mark NSTableView Delegate - (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { NSString *columnId = tableColumn.identifier; if (tableView == self.channelListTable) { IRCChannelConfig *config = self.channelList[row]; if ([columnId isEqualToString:@"name"]) { return config.channelName; } else if ([columnId isEqualToString:@"pass"]) { NSString *secretKeyValue = config.secretKey; if (secretKeyValue) { return secretKeyValue; } return @""; } else if ([columnId isEqualToString:@"join"]) { return @(config.autoJoin); } } else if (tableView == self.highlightsTable) { IRCHighlightMatchCondition *config = self.highlightList[row]; if ([columnId isEqualToString:@"keyword"]) { return config.matchKeyword; } else if ([columnId isEqualToString:@"channel"]) { NSString *matchChannelId = config.matchChannelId; if (matchChannelId) { for (IRCChannelConfig *channel in self.channelList) { if ([channel.uniqueIdentifier isEqualToString:matchChannelId] == NO) { continue; } return channel.channelName; } } return TXTLS(@"TDCServerPropertiesSheet[61f-6b]"); } else if ([columnId isEqualToString:@"type"]) { if (config.matchIsExcluded) { return TXTLS(@"TDCServerPropertiesSheet[qcc-b4]"); } else { return TXTLS(@"TDCServerPropertiesSheet[tet-dk]"); } } } else if (tableView == self.addressBookTable) { IRCAddressBookEntry *config = self.addressBookList[row]; if ([columnId isEqualToString:@"hostmask"]) { return config.hostmask; } else if ([columnId isEqualToString:@"type"]) { if (config.entryType == IRCAddressBookEntryTypeIgnore) { return TXTLS(@"TDCServerPropertiesSheet[f7o-x4]"); } else { return TXTLS(@"TDCServerPropertiesSheet[b0g-0x]"); } } } return nil; } - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row { return YES; } - (void)tableView:(NSTableView *)tableView setObjectValue:(nullable id)object forTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { NSString *columnId = tableColumn.identifier; if (tableView == self.channelListTable) { IRCChannelConfigMutable *config = [self.channelList[row] mutableCopy]; if ([columnId isEqualToString:@"join"]) { config.autoJoin = [object boolValue]; } [self.channelListArrayController replaceObjectAtArrangedObjectIndex:row withObject:[config copy]]; } } - (void)tableViewSelectionDidChange:(NSNotification *)aNote { NSTableView *tableView = aNote.object; if (tableView == self.channelListTable) { [self updateChannelListPage]; } else if (tableView == self.highlightsTable) { [self updateHighlightsPage]; } else if (tableView == self.addressBookTable) { [self updateAddressBookPage]; } } - (void)tableViewDoubleClicked:(id)sender { if (sender == self.channelListTable) { [self editChannel:sender]; } else if (sender == self.highlightsTable) { [self editHighlight:sender]; } else if (sender == self.addressBookTable) { [self editAddressBookEntry:sender]; } } - (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pasteboard { NSData *draggedData = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes]; [pasteboard declareTypes:@[_tableDragToken] owner:self]; [pasteboard setData:draggedData forType:_tableDragToken]; return YES; } - (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id )info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation { return NSDragOperationGeneric; } - (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id )info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation { NSPasteboard *pasteboard = [info draggingPasteboard]; NSData *draggedData = [pasteboard dataForType:_tableDragToken]; NSIndexSet *draggedRowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:draggedData]; NSUInteger draggedRowIndex = draggedRowIndexes.firstIndex; if (tableView == self.channelListTable) { [self clearChannelListPredicate]; [self.channelListArrayController moveObjectAtArrangedObjectIndex:draggedRowIndex toIndex:row]; [self setChannelListPredicate]; } else if (tableView == self.highlightsTable) { [self.highlightListArrayController moveObjectAtArrangedObjectIndex:draggedRowIndex toIndex:row]; } else if (tableView == self.addressBookTable) { [self.addressBookArrayController moveObjectAtArrangedObjectIndex:draggedRowIndex toIndex:row]; } return YES; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { self.addressBookTable.delegate = nil; self.channelListTable.delegate = nil; self.highlightsTable.delegate = nil; self.addressBookTable.dataSource = nil; self.channelListTable.dataSource = nil; self.highlightsTable.dataSource = nil; [self.addressBookTable unregisterDraggedTypes]; [self.channelListTable unregisterDraggedTypes]; [self.highlightsTable unregisterDraggedTypes]; [self.sheet makeFirstResponder:nil]; if ([self.delegate respondsToSelector:@selector(serverPropertiesSheetWillClose:)]) { [self.delegate serverPropertiesSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCSheetBase.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCValidatedComboBox.h" #import "TVCValidatedTextField.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @implementation TDCSheetBase - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithWindow:(nullable NSWindow *)window { if ((self = [super init])) { self.window = window; return self; } return nil; } - (void)startSheet { [self startSheetWithWindow:self.window]; } - (void)startSheetWithWindow:(NSWindow *)window { [window beginSheet:self.sheet completionHandler:^(NSModalResponse returnCode) { [self sheetDidEndWithReturnCode:returnCode]; }]; } - (void)endSheet { [self.window endSheet:self.sheet]; } - (void)sheetDidEndWithReturnCode:(NSInteger)returnCode { [self.sheet close]; } - (void)ok:(id)sender { [self endSheet]; } - (BOOL)okOrErrorForTextField:(TVCValidatedTextField *)textField { NSParameterAssert(textField != nil); return ([textField showValidationErrorPopover] == NO); } - (BOOL)okOrErrorForComboBox:(TVCValidatedComboBox *)comboBox { NSParameterAssert(comboBox != nil); return ([comboBox showValidationErrorPopover] == NO); } - (void)cancel:(id)sender { [self endSheet]; } - (void)close { [self cancel:nil]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCWelcomeSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSStringHelper.h" #import "IRCChannelConfig.h" #import "IRCClientConfig.h" #import "IRCNetworkList.h" #import "IRCServer.h" #import "TLOLocalization.h" #import "TPCPreferencesLocal.h" #import "TVCBasicTableView.h" #import "TVCValidatedComboBox.h" #import "TVCValidatedTextField.h" #import "TDCWelcomeSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCWelcomeSheet () @property (nonatomic, weak) IBOutlet NSButton *autoConnectCheck; @property (nonatomic, weak) IBOutlet NSButton *addChannelButton; @property (nonatomic, weak) IBOutlet NSButton *deleteChannelButton; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *nicknameTextField; @property (nonatomic, weak) IBOutlet TVCValidatedComboBox *serverAddressComboBox; @property (nonatomic, weak) IBOutlet TVCBasicTableView *channelTable; @property (nonatomic, strong) NSMutableArray *channelList; @property (nonatomic, strong) IRCNetworkList *networkList; - (IBAction)onAddChannel:(id)sender; - (IBAction)onDeleteChannel:(id)sender; @end @implementation TDCWelcomeSheet #pragma mark - #pragma mark Init. - (instancetype)initWithWindow:(nullable NSWindow *)window { if ((self = [super initWithWindow:window])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TDCWelcomeSheet" owner:self topLevelObjects:nil]; /* Populate server list combo box */ self.networkList = [IRCNetworkList new]; NSArray *listOfNetworks = self.networkList.listOfNetworks; for (IRCNetwork *network in listOfNetworks) { [self.serverAddressComboBox addItemWithObjectValue:network.networkName]; } /* Nickname */ self.nicknameTextField.textDidChangeCallback = self; self.nicknameTextField.stringValueIsInvalidOnEmpty = YES; self.nicknameTextField.stringValueIsTrimmed = YES; self.nicknameTextField.stringValueUsesOnlyFirstToken = YES; self.nicknameTextField.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isHostmaskNickname == NO) { return TXTLS(@"CommonErrors[och-j5]"); } return nil; }; /* Server address */ self.serverAddressComboBox.textDidChangeCallback = self; self.serverAddressComboBox.stringValueIsInvalidOnEmpty = YES; self.serverAddressComboBox.stringValueIsTrimmed = YES; self.serverAddressComboBox.stringValueUsesOnlyFirstToken = YES; self.serverAddressComboBox.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.isValidInternetAddress == NO) { return TXTLS(@"CommonErrors[yyx-l3]"); } return nil; }; /* Setup others */ self.channelList = [NSMutableArray new]; self.channelTable.textEditingDelegate = self; [self updateDeleteChannelButton]; self.nicknameTextField.stringValue = [TPCPreferences defaultNickname]; } #pragma mark - #pragma mark Controls - (void)start { [self startSheet]; } - (void)close { [super cancel:nil]; } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } IRCClientConfigMutable *config = nil; NSString *serverAddress = self.serverAddressComboBox.value; IRCNetwork *serverAddressNetwork = [self.networkList networkNamed:serverAddress]; if (serverAddressNetwork == nil) { serverAddressNetwork = [self.networkList networkWithServerAddress:serverAddress]; } if (serverAddressNetwork) { config = [IRCClientConfigMutable newConfigWithNetwork:serverAddressNetwork]; } else { serverAddress = serverAddress.lowercaseString; config = [IRCClientConfigMutable new]; config.connectionName = serverAddress; IRCServerMutable *server = [IRCServerMutable new]; server.serverAddress = serverAddress; config.serverList = @[[server copy]]; } config.autoConnect = (self.autoConnectCheck.state == NSControlStateValueOn); config.nickname = self.nicknameTextField.value; NSMutableArray *channelList = [NSMutableArray array]; NSMutableArray *channelsAdded = [NSMutableArray array]; for (NSString *channel in self.channelList) { NSString *channelName = channel.trim; if (channelName.length == 0) { continue; } if ([channelsAdded containsObjectIgnoringCase:channelName] == NO) { [channelsAdded addObject:channelName]; } else { continue; } if (channelName.isChannelName == NO) { channelName = [@"#" stringByAppendingString:channelName]; } IRCChannelConfig *channelConfig = [IRCChannelConfig seedWithName:channelName]; [channelList addObject:channelConfig]; } config.channelList = channelList; if ([self.delegate respondsToSelector:@selector(welcomeSheet:onOk:)]) { [self.delegate welcomeSheet:self onOk:[config copy]]; } [super ok:nil]; } - (BOOL)okOrError { if ([self okOrErrorForComboBox:self.serverAddressComboBox] == NO) { return NO; } if ([self okOrErrorForTextField:self.nicknameTextField] == NO) { return NO; } return YES; } - (void)onAddChannel:(id)sender { [self.channelList addObject:@""]; [self.channelTable reloadData]; NSInteger rowToEdit = (self.channelList.count - 1); [self.channelTable selectItemAtIndex:rowToEdit]; [self.channelTable editColumn:0 row:rowToEdit withEvent:nil select:YES]; } - (void)onDeleteChannel:(id)sender { NSInteger selectedRow = self.channelTable.selectedRow; if (selectedRow < 0) { return; } [self.channelList removeObjectAtIndex:selectedRow]; [self.channelTable reloadData]; NSInteger channelListCount = self.channelList.count; if (selectedRow > channelListCount) { selectedRow = (channelListCount - 1); } if (channelListCount >= 0) { [self.channelTable selectItemAtIndex:selectedRow]; } [self updateDeleteChannelButton]; } - (void)updateDeleteChannelButton { self.deleteChannelButton.enabled = (self.channelTable.numberOfSelectedRows > 0); } #pragma mark - #pragma mark NSTableView Delegate - (void)textDidEndEditing:(NSNotification *)note { NSInteger editedRow = self.channelTable.editedRow; if (editedRow < 0) { return; } NSString *editedString = [note.object textStorage].string; self.channelList[editedRow] = [editedString copy]; [self.channelTable reloadData]; [self.channelTable selectItemAtIndex:editedRow]; [self updateDeleteChannelButton]; } - (NSInteger)numberOfRowsInTableView:(NSTableView *)sender { return self.channelList.count; } - (id)tableView:(NSTableView *)sender objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row { return self.channelList[row]; } - (void)tableViewSelectionIsChanging:(NSNotification *)note { [self updateDeleteChannelButton]; } #pragma mark - #pragma mark NSWindow Delegate - (void)windowWillClose:(NSNotification *)note { self.channelTable.dataSource = nil; self.channelTable.delegate = nil; if ([self.delegate respondsToSelector:@selector(welcomeSheetWillClose:)]) { [self.delegate welcomeSheetWillClose:self]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Dialogs/TDCWindowBase.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCWindowBase.h" NS_ASSUME_NONNULL_BEGIN @implementation TDCWindowBase - (void)show { [self.window makeKeyAndOrderFront:nil]; } - (void)close { [self.window close]; } - (void)ok:(id)sender { [self close]; } - (void)cancel:(id)sender { [self close]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/External Libraries/GTMEncodeHTML.h ================================================ // // GTMNSString+HTML.h // Dealing with NSStrings that contain HTML // // Copyright 2006-2008 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. // #import NS_ASSUME_NONNULL_BEGIN /// Utilities for NSStrings containing HTML @interface NSString (GTMNSStringHTMLAdditions) /// Get a string where internal characters that need escaping for HTML are escaped /// /// For example, '&' become '&'. This will only cover characters from table /// A.2.2 of http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters /// which is what you want for a unicode encoded webpage. If you have a ascii /// or non-encoded webpage, please use stringByEscapingAsciiHTML which will /// encode all characters. /// /// For obvious reasons this call is only safe once. /// /// Returns: /// Autoreleased NSString /// @property (readonly, copy, nullable) NSString *gtm_stringByEscapingForHTML; /// Get a string where internal characters that need escaping for HTML are escaped /// /// For example, '&' become '&' /// All non-mapped characters (unicode that don't have a &keyword; mapping) /// will be converted to the appropriate &#xxx; value. If your webpage is /// unicode encoded (UTF16 or UTF8) use stringByEscapingHTML instead as it is /// faster, and produces less bloated and more readable HTML (as long as you /// are using a unicode compliant HTML reader). /// /// For obvious reasons this call is only safe once. /// /// Returns: /// Autoreleased NSString /// @property (readonly, copy) NSString *gtm_stringByEscapingForAsciiHTML; /// Get a string where internal characters that are escaped for HTML are unescaped /// /// For example, '&' becomes '&' /// Handles and 2 cases as well /// /// Returns: /// Autoreleased NSString /// @property (readonly, copy) NSString *gtm_stringByUnescapingFromHTML; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/External Libraries/OELReachability.h ================================================ /* Copyright (c) 2011, Tony Million. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import NS_ASSUME_NONNULL_BEGIN @class OELReachability; typedef void (^OELReachabilityNetworkReachableBlock)(OELReachability *reachability); typedef void (^OELReachabilityNetworkUnreachableBlock)(OELReachability *reachability); @interface OELReachability : NSObject @property (nonatomic, copy, nullable) OELReachabilityNetworkReachableBlock reachableBlock; @property (nonatomic, copy, nullable) OELReachabilityNetworkUnreachableBlock unreachableBlock; + (nullable OELReachability *)reachabilityForInternetConnection; @property (getter=isReachable, readonly) BOOL reachable; - (BOOL)startNotifier; - (void)stopNotifier; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRC.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Hard limits */ #define TXMaximumIRCBodyLength 510 #define TXMaximumIRCNicknameLength 50 #define TXMaximumIRCUsernameLength 40 #define TXMaximumTCPPort 65535 #define TXMaximumNodesPerModeCommand 4 #define IRCProtocolDefaultNicknameMaximumLength 31 ================================================ FILE: Sources/App/Classes/Headers/IRCAddressBook.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, IRCAddressBookEntryType) { IRCAddressBookEntryTypeIgnore = 0, IRCAddressBookEntryTypeUserTracking, /* Entry type used when multiple instances of IRCAddressBookEntry are combined into a single object which represents all. */ IRCAddressBookEntryTypeMixed }; typedef NS_ENUM(NSUInteger, IRCAddressBookUserTrackingStatus) { IRCAddressBookUserTrackingStatusUnknown = 0, IRCAddressBookUserTrackingStatusSignedOff, IRCAddressBookUserTrackingStatusSignedOn, IRCAddressBookUserTrackingStatusAvailable, IRCAddressBookUserTrackingStatusNotAvailable, IRCAddressBookUserTrackingStatusAway, IRCAddressBookUserTrackingStatusNotAway }; #pragma mark - #pragma mark Immutable Object @interface IRCAddressBookEntry : XRPortablePropertyDict @property (readonly) IRCAddressBookEntryType entryType; @property (readonly, copy) NSString *uniqueIdentifier; @property (readonly, copy) NSString *hostmask; @property (readonly, copy) NSString *hostmaskRegularExpression; @property (readonly, copy, nullable) NSString *trackingNickname; @property (readonly) BOOL ignoreClientToClientProtocol; @property (readonly) BOOL ignoreGeneralEventMessages; @property (readonly) BOOL ignoreNoticeMessages; @property (readonly) BOOL ignorePrivateMessageHighlights; @property (readonly) BOOL ignorePrivateMessages; @property (readonly) BOOL ignorePublicMessageHighlights; @property (readonly) BOOL ignorePublicMessages; @property (readonly) BOOL ignoreFileTransferRequests; @property (readonly) BOOL ignoreInlineMedia; @property (readonly) BOOL ignoreMessagesContainingMatch; @property (readonly) BOOL trackUserActivity; /* When IRCAddressBookEntryTypeMixed is mixed, this array holds a reference to each entry that is mixed into the current object. */ @property (readonly, copy, nullable) NSArray *parentEntries; + (instancetype)newIgnoreEntry; + (instancetype)newIgnoreEntryForHostmask:(nullable NSString *)hostmask; + (instancetype)newUserTrackingEntry; - (BOOL)checkMatch:(NSString *)hostmask; @end #pragma mark - #pragma mark Mutable Object @interface IRCAddressBookEntryMutable : IRCAddressBookEntry @property (nonatomic, assign, readwrite) IRCAddressBookEntryType entryType; @property (nonatomic, copy, readwrite) NSString *hostmask; @property (nonatomic, assign, readwrite) BOOL ignoreClientToClientProtocol; @property (nonatomic, assign, readwrite) BOOL ignoreGeneralEventMessages; @property (nonatomic, assign, readwrite) BOOL ignoreNoticeMessages; @property (nonatomic, assign, readwrite) BOOL ignorePrivateMessageHighlights; @property (nonatomic, assign, readwrite) BOOL ignorePrivateMessages; @property (nonatomic, assign, readwrite) BOOL ignorePublicMessageHighlights; @property (nonatomic, assign, readwrite) BOOL ignorePublicMessages; @property (nonatomic, assign, readwrite) BOOL ignoreFileTransferRequests; @property (nonatomic, assign, readwrite) BOOL ignoreInlineMedia; @property (nonatomic, assign, readwrite) BOOL trackUserActivity; @property (nonatomic, copy, readwrite, nullable) NSArray *parentEntries; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCAddressBookUserTracking.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCAddressBook.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient; TEXTUAL_EXTERN NSNotificationName const IRCAddressBookUserTrackingStatusChangedNotification; TEXTUAL_EXTERN NSNotificationName const IRCAddressBookUserTrackingAddedTrackedUserNotification; TEXTUAL_EXTERN NSNotificationName const IRCAddressBookUserTrackingRemovedTrackedUserNotification; TEXTUAL_EXTERN NSNotificationName const IRCAddressBookUserTrackingRemovedAllTrackedUsersNotification; @interface IRCAddressBookUserTrackingContainer : NSObject @property (readonly, weak) IRCClient *client; // value is boolean, whether tracked user is online @property (readonly, copy) NSDictionary *trackedUsers; - (IRCAddressBookUserTrackingStatus)statusOfUser:(NSString *)nickname; - (IRCAddressBookUserTrackingStatus)statusOfEntry:(IRCAddressBookEntry *)addressBookEntry; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCChannel.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelConfig.h" #import "IRCChannelMemberList.h" #import "IRCTreeItem.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannelMode, IRCChannelUser, IRCUser; @class TVCLogLine; typedef NS_ENUM(NSUInteger, IRCChannelStatus) { IRCChannelStatusParted = 0, IRCChannelStatusJoining, IRCChannelStatusJoined, IRCChannelStatusTerminated, }; TEXTUAL_EXTERN NSNotificationName const IRCChannelConfigurationWasUpdatedNotification; @interface IRCChannel : IRCTreeItem @property (readonly, copy) IRCChannelConfig *config; @property (nonatomic, copy) NSString *name; // -setName: will do nothing if type != IRCChannelTypePrivateMessage @property (nonatomic, copy, nullable) NSString *topic; @property (nonatomic, assign) BOOL autoJoin; @property (readonly) IRCChannelType type; @property (getter=isChannel, readonly) BOOL channel; @property (getter=isPrivateMessage, readonly) BOOL privateMessage; @property (getter=isPrivateMessageForZNCUser, readonly) BOOL privateMessageForZNCUser; // For example: *status, *nickserv, etc. @property (getter=isUtility, readonly) BOOL utility; // See IRCChannelTypeUtility in IRCChannelConfig.h @property (readonly) IRCChannelStatus status; @property (readonly) BOOL errorOnLastJoinAttempt; @property (readonly) NSTimeInterval channelJoinTime; @property (readonly, copy) NSString *channelTypeString; @property (readonly, strong, nullable) IRCChannelMode *modeInfo; @property (readonly, strong, nullable) IRCChannelMemberList *memberInfo; @property (readonly, copy, nullable) NSString *secretKey; @property (readonly, copy, nullable) NSURL *logFilePath; @property (readonly) NSUInteger logFileSessionCount; // Number of lines sent to channel log file for session (from connect to disconnect) @property (readonly, weak) TVCLogLine *lastLine; // Last line in the channel. There is no guarantee it's visible to the user when accessed. #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 @property (readonly) BOOL encryptionStateIsEncrypted; #endif - (instancetype)init NS_UNAVAILABLE; - (void)activate; - (void)deactivate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCChannelConfig.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLONotificationController.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, IRCChannelType) { IRCChannelTypeChannel = 0, IRCChannelTypePrivateMessage, IRCChannelTypeUtility, }; #pragma mark - #pragma mark Immutable Object @interface IRCChannelConfig : XRPortablePropertyDict @property (readonly) BOOL autoJoin; @property (readonly) BOOL ignoreGeneralEventMessages; @property (readonly) BOOL ignoreHighlights; @property (readonly) BOOL inlineMediaDisabled; @property (readonly) BOOL inlineMediaEnabled; @property (readonly) BOOL pushNotifications; @property (readonly) BOOL showTreeBadgeCount; @property (readonly) IRCChannelType type; @property (readonly, copy) NSString *channelName; @property (readonly, copy) NSString *uniqueIdentifier; @property (readonly, copy, nullable) NSString *label; @property (readonly, copy, nullable) NSString *defaultModes; @property (readonly, copy, nullable) NSString *defaultTopic; @property (readonly, copy, nullable) NSString *secretKey; @property (readonly, copy, nullable) NSString *secretKeyFromKeychain; + (IRCChannelConfig *)seedWithName:(NSString *)channelName; /* Notifications */ - (nullable NSString *)soundForEvent:(TXNotificationType)event; // These methods return an integer because there are more than // two possible values. When there is no channel defined value // for the given event, NSControlStateValueMixed is returned // which indicates that the global value should be used. // NSControlStateValueOn and NSControlStateValueOff // are returned when a channel defined value is available. - (NSControlStateValue)notificationEnabledForEvent:(TXNotificationType)event; - (NSControlStateValue)disabledWhileAwayForEvent:(TXNotificationType)event; - (NSControlStateValue)bounceDockIconForEvent:(TXNotificationType)event; - (NSControlStateValue)bounceDockIconRepeatedlyForEvent:(TXNotificationType)event; - (NSControlStateValue)speakEvent:(TXNotificationType)event; @end #pragma mark - #pragma mark Mutable Object @interface IRCChannelConfigMutable : IRCChannelConfig @property (nonatomic, assign, readwrite) IRCChannelType type; @property (nonatomic, assign, readwrite) BOOL autoJoin; @property (nonatomic, assign, readwrite) BOOL ignoreGeneralEventMessages; @property (nonatomic, assign, readwrite) BOOL ignoreHighlights; @property (nonatomic, assign, readwrite) BOOL inlineMediaDisabled; @property (nonatomic, assign, readwrite) BOOL inlineMediaEnabled; @property (nonatomic, assign, readwrite) BOOL pushNotifications; @property (nonatomic, assign, readwrite) BOOL showTreeBadgeCount; @property (nonatomic, copy, readwrite) NSString *channelName; @property (nonatomic, copy, readwrite, nullable) NSString *label; @property (nonatomic, copy, readwrite, nullable) NSString *defaultModes; @property (nonatomic, copy, readwrite, nullable) NSString *defaultTopic; @property (nonatomic, copy, readwrite, nullable) NSString *secretKey; - (void)setSound:(nullable NSString *)value forEvent:(TXNotificationType)event; // NSControlStateValueOn = YES // NSControlStateValueOff = NO // NSControlStateValueMixed = Reset, use default - (void)setNotificationEnabled:(NSControlStateValue)value forEvent:(TXNotificationType)event; - (void)setDisabledWhileAway:(NSControlStateValue)value forEvent:(TXNotificationType)event; - (void)setBounceDockIcon:(NSControlStateValue)value forEvent:(TXNotificationType)event; - (void)setBounceDockIconRepeatedly:(NSControlStateValue)value forEvent:(TXNotificationType)event; - (void)setEventIsSpoken:(NSControlStateValue)value forEvent:(TXNotificationType)event; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCChannelMemberList.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCChannelUser, IRCUser; #pragma mark - #pragma mark Prototype /* IRCChannel proxies many methods declared by IRCChannelMemberList for backwards compatibility and convenience through the use of IRCChannelMemberListPrototype protocol. It's atypical to access IRCChannelMemberList directly. The declaration for it is only public to allow KVO binding to -memberList and -numberOfMembers. */ /* In context of IRCChannel proxied methods: An instance of IRCChannel will only have a member list when in active state. This includes regular channels and private messages. Utility channels will never have a member list. All proxied methods will do nothing when there is no member list. */ @protocol IRCChannelMemberListPrototype /* Member changes (adding, removing, modifying) are done so asynchronously. This means that changes wont be immediately reflected by -memberList. */ /* It is safe to call -memberExists: and -findMember: immediately after changing a member because those methods do not require the member to be present in the member list to produce a result. */ - (void)addUser:(IRCUser *)user; - (void)addMember:(IRCChannelUser *)member; - (void)removeMember:(IRCChannelUser *)member; - (void)removeMemberWithNickname:(NSString *)nickname; - (BOOL)memberExists:(NSString *)nickname; - (nullable IRCChannelUser *)findMember:(NSString *)nickname; /* -memberList and -numberOfMembers are KVO compliant when bound directly to an instance of IRCChannelMemberList. The methods that IRCChannel proxy will not post KVO changes. */ @property (readonly) NSUInteger numberOfMembers; @property (readonly, copy, nullable) NSArray *memberList; // Automatically sorted by channel rank /* Resort the entire member list using all known conditions. */ /* This can be an expensive task for large channels. */ - (void)sortMembers; @end #pragma mark - #pragma mark Interface @interface IRCChannelMemberList : NSObject - (instancetype)init NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCChannelMode.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCChannelModeContainer, IRCModeInfo; @interface IRCChannelMode : NSObject - (BOOL)modeIsDefined:(NSString *)modeSymbol; - (nullable IRCModeInfo *)modeInfoFor:(NSString *)modeSymbol; @property (readonly, copy) IRCChannelModeContainer *modes; @property (readonly, copy) NSString *string; @property (readonly, copy) NSString *stringWithMaskedPassword; - (NSString *)getChangeCommand:(IRCChannelModeContainer *)modes; - (instancetype)init NS_UNAVAILABLE; @end #pragma mark - @interface IRCChannelModeContainer : NSObject @property (readonly, copy) NSDictionary *modes; - (BOOL)modeIsDefined:(NSString *)modeSymbol; - (nullable IRCModeInfo *)modeInfoFor:(NSString *)modeSymbol; - (void)changeMode:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet; - (void)changeMode:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameter:(nullable NSString *)modeParameter; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCChannelUser.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCUser, IRCUserMutable; typedef NS_OPTIONS(NSUInteger, IRCUserRank) { IRCUserRankNone = 1 << 0, // nothing IRCUserRankIRCopByMode = 1 << 1, // +y/+Y IRCUserRankChannelOwner = 1 << 2, // +q IRCUserRankSuperOperator = 1 << 3, // +a IRCUserRankNormalOperator = 1 << 4, // +o IRCUserRankHalfOperator = 1 << 5, // +h IRCUserRankVoiced = 1 << 6 // +v }; #pragma mark - #pragma mark Immutable Object @interface IRCChannelUser : XRPortablePropertyObject @property (readonly, strong) IRCUser *user; @property (getter=isOp, readonly) BOOL op; @property (getter=isHalfOp, readonly) BOOL halfOp; // -rank(s) returns IRCUserRankIRCopByMode if the +Y/+y modes defined // by InspIRCd-2.0 for IRC operators are in use by this user. It does not // return this if the user is an IRC operator, but lacks these modes. // Use -isCop for the status of the user regardless of these modes. @property (readonly) IRCUserRank rank; // Highest rank user has @property (readonly) IRCUserRank ranks; // All ranks user as a bitmask @property (readonly, copy) NSString *modes; // List of all user modes, ranked highest to lowest @property (readonly, copy) NSString *mark; // Returns mode symbol for highest rank (-modes) /* Weight used when completing nicknames. */ /* Accessing totalWeight decays the weight. */ /* Accessing other weight properties does not. */ @property (readonly) double totalWeight; // (incoming + outgoing) with decay @property (readonly) double incomingWeight; @property (readonly) double outgoingWeight; /* Timestamp instance of IRCChannelUser was created. */ @property (readonly) NSTimeInterval creationTime; - (instancetype)init NS_UNAVAILABLE; @end #pragma mark - #pragma mark Mutable Object @interface IRCChannelUserMutable : IRCChannelUser @property (nonatomic, copy, readwrite) NSString *modes; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCClient.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCCommandIndex.h" #import "IRCConnection.h" #import "IRCTreeItem.h" #import "TLOEncryptionManager.h" #import "TVCLogController.h" #import "TVCLogLine.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel, IRCClientConfig, IRCHighlightLogEntry, IRCISupportInfo; @class IRCAddressBookEntry, IRCMessage, IRCServer, IRCUser; typedef NS_ENUM(NSUInteger, IRCClientConnectMode) { IRCClientConnectModeNormal = 0, IRCClientConnectModeRetry, IRCClientConnectModeReconnect, }; typedef NS_ENUM(NSUInteger, IRCClientDisconnectMode) { IRCClientDisconnectModeNormal = 0, IRCClientDisconnectModeComputerSleep, IRCClientDisconnectModeBadCertificate, IRCClientDisconnectModeReachabilityChange, IRCClientDisconnectModeServerRedirect }; typedef NS_OPTIONS(NSUInteger, ClientIRCv3SupportedCapability) { ClientIRCv3SupportedCapabilityAwayNotify = 1 << 0, // YES if away-notify CAP supported ClientIRCv3SupportedCapabilityBatch = 1 << 1, // YES if batch CAP supported ClientIRCv3SupportedCapabilityEchoMessage = 1 << 2, // YES if echo-message CAP supported ClientIRCv3SupportedCapabilityIdentifyCTCP = 1 << 3, // YES if identify-ctcp CAP supported ClientIRCv3SupportedCapabilityIdentifyMsg = 1 << 4, // YES if identify-msg CAP supported ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL = 1 << 5, // YES if SASL authentication was successful ClientIRCv3SupportedCapabilityIsInSASLNegotiation = 1 << 6, // YES if in SASL CAP authentication request ClientIRCv3SupportedCapabilityMonitorCommand = 1 << 7, // YES if the MONITOR command is supported ClientIRCv3SupportedCapabilityMultiPrefix = 1 << 8, // YES if multi-prefix CAP supported ClientIRCv3SupportedCapabilityPlayback = 1 << 9, // Special CAP which is subject to change ClientIRCv3SupportedCapabilityServerTime = 1 << 10, // YES if server-time CAP supported ClientIRCv3SupportedCapabilityUserhostInNames = 1 << 11, // YES if userhost-in-names CAP supported ClientIRCv3SupportedCapabilityWatchCommand = 1 << 12, // YES if the WATCH command is supported ClientIRCv3SupportedCapabilityZNCCertInfoModule = 1 << 13, // YES if the ZNC vendor specific CAP supported ClientIRCv3SupportedCapabilityZNCSelfMessage = 1 << 14, // YES if the ZNC vendor specific CAP supported ClientIRCv3SupportedCapabilityChangeHost = 1 << 15 // YES if the CHGHOST CAP supported }; TEXTUAL_EXTERN NSNotificationName const IRCClientConfigurationWasUpdatedNotification; TEXTUAL_EXTERN NSNotificationName const IRCClientChannelListWasModifiedNotification; TEXTUAL_EXTERN NSNotificationName const IRCClientWillConnectNotification; TEXTUAL_EXTERN NSNotificationName const IRCClientDidConnectNotification; TEXTUAL_EXTERN NSNotificationName const IRCClientWillSendQuitNotification; TEXTUAL_EXTERN NSNotificationName const IRCClientWillDisconnectNotification; TEXTUAL_EXTERN NSNotificationName const IRCClientDidDisconnectNotification; TEXTUAL_EXTERN NSNotificationName const IRCClientUserNicknameChangedNotification; @interface IRCClient : IRCTreeItem @property (readonly, copy) IRCClientConfig *config; @property (readonly, copy, nullable) IRCServer *server; // Where is being connected to. Use -serverAddress for server address connected to. @property (readonly) IRCISupportInfo *supportInfo; @property (readonly) IRCClientConnectMode connectType; @property (readonly) IRCClientDisconnectMode disconnectType; @property (readonly) BOOL isAutojoined; // YES if autojoin has completed @property (readonly) BOOL isAutojoining; // YES if autojoin is in progress @property (readonly) BOOL isConnecting; // YES if socket is connecting. Set to NO on raw numeric 001. @property (readonly) BOOL isConnected; // YES if socket is connected @property (readonly) BOOL isConnectedToZNC; // YES if Textual detected that this connection is ZNC @property (readonly) BOOL isLoggedIn; // YES if logged into server. Set to YES on raw numeric 001. @property (readonly) BOOL isQuitting; // YES if socket is disconnecting @property (readonly) BOOL isReconnecting; // YES if reconnect is pending @property (readonly) BOOL isSecured; // YES if socket is connected using SSL/TLS @property (readonly) BOOL userIsAway; // YES if local user is away @property (readonly) BOOL userIsIRCop; // YES if local user is IRCop @property (readonly) BOOL userIsIdentifiedWithNickServ; // YES if NickServ identification was successful @property (readonly) BOOL isWaitingForNickServ; // YES if NickServ identification is pending @property (readonly) BOOL serverHasNickServ; // YES if NickServ service was found on server @property (readonly) NSTimeInterval lastMessageReceived; // The time at which the last of any incoming data was received @property (readonly) NSTimeInterval lastMessageServerTime; // The time of the last message received that contained a server-time CAP @property (readonly) NSUInteger channelCount; @property (readonly, weak) IRCChannel *lastSelectedChannel; // If this is the selected client, then the value of this property is the current selection. If the current client is not selected, then this value is either its previous selection or nil. @property (readonly, weak) TVCLogLine *lastLine; // Last line in the server console. There is no guarantee it's visible to the user when accessed. @property (readonly, copy) NSArray *channelList; @property (readonly, copy) NSArray *cachedHighlights; @property (readonly, copy, nullable) NSString *userHostmask; // The hostmask of the local user @property (readonly, copy) NSString *userNickname; // The nickname of the local user @property (readonly, copy, nullable) NSString *serverAddress; // The address of the server connected to or nil @property (readonly, copy, nullable) NSString *networkName; // The name of the network connected to or nil @property (readonly, copy) NSString *networkNameAlt; // The name of the network connected to or the configured Connection Name @property (readonly, copy, nullable) NSString *preAwayUserNickname; // Nickname before away was set or nil @property (readonly, copy, nullable) NSData *zncBouncerCertificateChainData; @property (readonly) NSUInteger logFileSessionCount; // Number of lines sent to server console log file for session (from connect to disconnect) - (instancetype)init NS_UNAVAILABLE; - (void)connect; - (void)connect:(IRCClientConnectMode)connectMode; - (void)connect:(IRCClientConnectMode)connectMode bypassProxy:(BOOL)bypassProxy; - (void)quit; - (void)quitWithComment:(NSString *)comment; - (void)cancelReconnect; @property (readonly) ClientIRCv3SupportedCapability capacities; @property (readonly, copy) NSString *enabledCapacitiesStringValue; - (BOOL)isCapabilitySupported:(NSString *)capabilityString; - (BOOL)isCapabilityEnabled:(ClientIRCv3SupportedCapability)capability; - (void)joinChannel:(IRCChannel *)channel; - (void)joinChannel:(IRCChannel *)channel password:(nullable NSString *)password; - (void)joinChannels:(NSArray *)channels; - (void)joinUnlistedChannel:(NSString *)channel; - (void)joinUnlistedChannel:(NSString *)channel password:(nullable NSString *)password; - (void)forceJoinChannel:(NSString *)channel password:(nullable NSString *)password; - (void)partChannel:(IRCChannel *)channel; - (void)partChannel:(IRCChannel *)channel withComment:(nullable NSString *)comment; - (void)partUnlistedChannel:(NSString *)channel; - (void)partUnlistedChannel:(NSString *)channel withComment:(nullable NSString *)comment; - (void)changeNickname:(NSString *)newNickname; - (void)kick:(NSString *)nickname inChannel:(IRCChannel *)channel; - (void)sendCTCPQuery:(NSString *)nickname command:(NSString *)command text:(nullable NSString *)text; - (void)sendCTCPReply:(NSString *)nickname command:(NSString *)command text:(nullable NSString *)text; - (void)sendCTCPPing:(NSString *)nickname; - (void)sendWhois:(NSString *)nickname; - (void)sendWhoToChannel:(IRCChannel *)channel; - (void)sendWhoToChannelNamed:(NSString *)channel; - (void)toggleAwayStatus:(BOOL)setAway; - (void)toggleAwayStatus:(BOOL)setAway withComment:(nullable NSString *)comment; - (void)requestModesForChannel:(IRCChannel *)channel; - (void)requestModesForChannelNamed:(NSString *)channel; - (void)sendModes:(nullable NSString *)modeSymbols withParameters:(nullable NSArray *)parameters inChannel:(IRCChannel *)channel; - (void)sendModes:(nullable NSString *)modeSymbols withParametersString:(nullable NSString *)parametersString inChannel:(IRCChannel *)channel; - (void)sendModes:(nullable NSString *)modeSymbols withParameters:(nullable NSArray *)parameters inChannelNamed:(NSString *)channel; - (void)sendModes:(nullable NSString *)modeSymbols withParametersString:(nullable NSString *)parametersString inChannelNamed:(NSString *)channel; - (void)sendPing:(NSString *)tokenString; - (void)sendPong:(NSString *)tokenString; - (void)sendInviteTo:(NSString *)nickname toJoinChannel:(IRCChannel *)channel; - (void)sendInviteTo:(NSString *)nickname toJoinChannelNamed:(NSString *)channel; - (void)requestTopicForChannel:(IRCChannel *)channel; - (void)requestTopicForChannelNamed:(NSString *)channel; - (void)sendTopicTo:(nullable NSString *)topic inChannel:(IRCChannel *)channel; - (void)sendTopicTo:(nullable NSString *)topic inChannelNamed:(NSString *)channel; - (void)sendCapability:(NSString *)subcommand data:(nullable NSString *)data; - (void)sendIsonForNicknames:(NSArray *)nicknames; - (void)modifyWatchListBy:(BOOL)adding nicknames:(NSArray *)nicknames; - (void)requestChannelList; - (NSArray *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet parameterString:(NSString *)parameterString; - (NSArray *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet parameterString:(NSString *)parameterString characterSet:(NSCharacterSet *)characterList; - (NSArray *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameters:(NSArray *)modeParameters; - (void)createChannelListDialog; - (void)createChannelInviteExceptionListSheet; - (void)createChannelBanExceptionListSheet; - (void)createChannelBanListSheet; - (void)createChannelQuietListSheet; - (void)presentCertificateTrustInformation; - (void)closeDialogs; #pragma mark - - (BOOL)userExists:(NSString *)nickname; - (nullable IRCUser *)findUser:(NSString *)nickname; - (IRCUser *)findUserOrCreate:(NSString *)nickname; @property (readonly) NSUInteger numberOfUsers; @property (readonly, copy) NSArray *userList; - (void)addUser:(IRCUser *)user; - (void)removeUser:(IRCUser *)user; - (void)removeUserWithNickname:(NSString *)nickname; @property (readonly, nullable) IRCUser *myself; - (NSArray *)findIgnoresForHostmask:(NSString *)hostmask; #pragma mark - - (nullable IRCChannel *)findChannel:(NSString *)withName; - (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName; - (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName isPrivateMessage:(BOOL)isPrivateMessage; - (nullable NSData *)convertToCommonEncoding:(NSString *)string; - (nullable NSString *)convertFromCommonEncoding:(NSData *)data; - (NSString *)formatNickname:(NSString *)nickname inChannel:(nullable IRCChannel *)channel; - (NSString *)formatNickname:(NSString *)nickname inChannel:(nullable IRCChannel *)channel withFormat:(nullable NSString *)format; - (BOOL)nicknameIsZNCUser:(NSString *)nickname; - (BOOL)nickname:(NSString *)nickname isZNCUser:(NSString *)zncNickname; - (nullable NSString *)nicknameAsZNCUser:(NSString *)nickname; // Returns nil if not connected to ZNC - (BOOL)nicknameIsMyself:(NSString *)nickname; - (BOOL)stringIsNickname:(NSString *)string; - (BOOL)stringIsChannelName:(NSString *)string; - (BOOL)outputRuleMatchedInMessage:(NSString *)message inChannel:(nullable IRCChannel *)channel; #pragma mark - - (void)setUnreadStateForChannel:(IRCChannel *)channel; - (void)setUnreadStateForChannel:(IRCChannel *)channel isHighlight:(BOOL)isHighlight; - (void)setHighlightStateForChannel:(IRCChannel *)channel; #pragma mark - - (void)sendCommand:(id)string; - (void)sendCommand:(id)string completeTarget:(BOOL)completeTarget target:(nullable NSString *)targetChannelName; - (void)sendCommand:(NSString *)command toZNCModuleNamed:(NSString *)module; - (void)sendText:(NSAttributedString *)string asCommand:(IRCRemoteCommand)command toChannel:(IRCChannel *)channel; - (void)sendText:(NSAttributedString *)string asCommand:(IRCRemoteCommand)command toChannel:(IRCChannel *)channel withEncryption:(BOOL)encryptText; - (void)sendLine:(NSString *)string; - (void)send:(NSString *)string, ...; - (void)sendPrivmsg:(NSString *)message toChannel:(IRCChannel *)channel; // Invoke -sendText: with proper values - (void)sendAction:(NSString *)message toChannel:(IRCChannel *)channel; - (void)sendNotice:(NSString *)message toChannel:(IRCChannel *)channel; #pragma mark - #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (NSUInteger)lengthOfEncryptedMessageDirectedAt:(NSString *)messageTo thatFitsWithinBounds:(NSUInteger)maximumLength; - (BOOL)encryptionAllowedForTarget:(NSString *)target; - (void)encryptMessage:(NSString *)messageBody directedAt:(NSString *)messageTo encodingCallback:(TLOEncryptionManagerEncodingDecodingCallbackBlock)encodingCallback injectionCallback:(TLOEncryptionManagerInjectCallbackBlock)injectionCallback; - (void)decryptMessage:(NSString *)messageBody from:(NSString *)messageFrom target:(NSString *)target decodingCallback:(TLOEncryptionManagerEncodingDecodingCallbackBlock)decodingCallback; @property (nonatomic, readonly, copy) NSString * _Nonnull encryptionAccountNameForLocalUser; - (NSString *)encryptionAccountNameForUser:(NSString *)nickname; - (void)encryptionAuthenticateUser:(NSString *)nickname; #endif #pragma mark - // nil channel prints the message to the server console // referenceMessage.command is used if command == nil // referenceMessage and command cannot be nil together (this throws exceptions) - (void) print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted escapeMessage:(BOOL)escapeMessage referenceMessage:(nullable IRCMessage *)referenceMessage completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock; - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command; - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command receivedAt:(NSDate *)receivedAt; - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted; - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted referenceMessage:(nullable IRCMessage *)referenceMessage; - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted referenceMessage:(nullable IRCMessage *)referenceMessage completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock; - (void)printDebugInformationToConsole:(NSString *)message; - (void)printDebugInformationToConsole:(NSString *)message asCommand:(NSString *)command; - (void)printDebugInformation:(NSString *)message; - (void)printDebugInformation:(NSString *)message asCommand:(NSString *)command; - (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel; - (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel asCommand:(NSString *)command; #pragma mark - - (void)clearCachedHighlights; /* -config may not always reflect the current state of the client. * This is because its too costly to mutate it for stuff that changes * many times a second. The client instead saves a copy of its * configuration periodically. This method will force it to perform * a save if you need to rely on most recent version. */ - (void)updateStoredConfiguration; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCClientConfig.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCConnectionConfig.h" NS_ASSUME_NONNULL_BEGIN @class IRCAddressBookEntry, IRCChannelConfig, IRCHighlightMatchCondition; @class IRCNetwork, IRCServer; #pragma mark - #pragma mark Immutable Object @interface IRCClientConfig : XRPortablePropertyDict @property (readonly) BOOL autoConnect; @property (readonly) BOOL autoReconnect; @property (readonly) BOOL autoSleepModeDisconnect; @property (readonly) BOOL autojoinWaitsForNickServ; @property (readonly) BOOL hideAutojoinDelayedWarnings; @property (readonly) BOOL hideNetworkUnavailabilityNotices; @property (readonly) BOOL performDisconnectOnPongTimer; @property (readonly) BOOL performDisconnectOnReachabilityChange; @property (readonly) BOOL performPongTimer; @property (readonly) BOOL saslAuthenticationDisableExternalMechanism; @property (readonly) BOOL sendAuthenticationRequestsToUserServ; @property (readonly) BOOL sendWhoCommandRequestsToChannels; @property (readonly) BOOL setInvisibleModeOnConnect; @property (readonly) BOOL sidebarItemExpanded; @property (readonly) BOOL validateServerCertificateChain; @property (readonly) BOOL zncIgnoreConfiguredAutojoin; @property (readonly) BOOL zncIgnorePlaybackNotifications; @property (readonly) BOOL zncIgnoreUserNotifications; @property (readonly) BOOL zncOnlyPlaybackLatest; @property (readonly) IRCConnectionAddressType addressType; @property (readonly) IRCConnectionProxyType proxyType; @property (readonly) NSStringEncoding fallbackEncoding; @property (readonly) NSStringEncoding primaryEncoding; @property (readonly) NSTimeInterval lastMessageServerTime; @property (readonly) NSUInteger floodControlDelayTimerInterval; @property (readonly) NSUInteger floodControlMaximumMessages; @property (readonly) uint16_t proxyPort; @property (readonly, copy) NSArray *channelList; @property (readonly, copy) NSArray *highlightList; @property (readonly, copy) NSArray *ignoreList; @property (readonly, copy) NSArray *alternateNicknames; @property (readonly, copy) NSArray *loginCommands; @property (readonly, copy) NSArray *serverList; @property (readonly, copy) NSString *connectionName; @property (readonly, copy) NSString *nickname; @property (readonly, copy) NSString *normalLeavingComment; @property (readonly, copy) NSString *realName; @property (readonly, copy) NSString *sleepModeLeavingComment; @property (readonly, copy) NSString *uniqueIdentifier; @property (readonly, copy) NSString *username; @property (readonly, copy, nullable) NSData *identityClientSideCertificate; @property (readonly, copy, nullable) NSString *awayNickname; @property (readonly, copy, nullable) NSString *nicknamePassword; @property (readonly, copy, nullable) NSString *nicknamePasswordFromKeychain; @property (readonly, copy, nullable) NSString *proxyAddress; @property (readonly, copy, nullable) NSString *proxyPassword; @property (readonly, copy, nullable) NSString *proxyPasswordFromKeychain; @property (readonly, copy, nullable) NSString *proxyUsername; @property (readonly) RCMCipherSuiteCollection cipherSuites; - (instancetype)initWithDictionary:(NSDictionary *)dic ignorePrivateMessages:(BOOL)ignorePrivateMessages NS_DESIGNATED_INITIALIZER; + (instancetype)newConfigByMerging:(IRCClientConfig *)config1 with:(IRCClientConfig *)config2; + (instancetype)newConfigWithNetwork:(IRCNetwork *)network; @end #pragma mark - #pragma mark Mutable Object @interface IRCClientConfigMutable : IRCClientConfig @property (nonatomic, assign, readwrite) BOOL autoConnect; @property (nonatomic, assign, readwrite) BOOL autoReconnect; @property (nonatomic, assign, readwrite) BOOL autoSleepModeDisconnect; @property (nonatomic, assign, readwrite) BOOL autojoinWaitsForNickServ; @property (nonatomic, assign, readwrite) BOOL hideAutojoinDelayedWarnings; @property (nonatomic, assign, readwrite) BOOL hideNetworkUnavailabilityNotices; @property (nonatomic, assign, readwrite) BOOL performDisconnectOnPongTimer; @property (nonatomic, assign, readwrite) BOOL performDisconnectOnReachabilityChange; @property (nonatomic, assign, readwrite) BOOL performPongTimer; @property (nonatomic, assign, readwrite) BOOL saslAuthenticationDisableExternalMechanism; @property (nonatomic, assign, readwrite) BOOL sendAuthenticationRequestsToUserServ; @property (nonatomic, assign, readwrite) BOOL sendWhoCommandRequestsToChannels; @property (nonatomic, assign, readwrite) BOOL setInvisibleModeOnConnect; @property (nonatomic, assign, readwrite) BOOL sidebarItemExpanded; @property (nonatomic, assign, readwrite) BOOL validateServerCertificateChain; @property (nonatomic, assign, readwrite) BOOL zncIgnoreConfiguredAutojoin; @property (nonatomic, assign, readwrite) BOOL zncIgnorePlaybackNotifications; @property (nonatomic, assign, readwrite) BOOL zncIgnoreUserNotifications; @property (nonatomic, assign, readwrite) BOOL zncOnlyPlaybackLatest; @property (nonatomic, assign, readwrite) IRCConnectionAddressType addressType; @property (nonatomic, assign, readwrite) IRCConnectionProxyType proxyType; @property (nonatomic, assign, readwrite) NSStringEncoding fallbackEncoding; @property (nonatomic, assign, readwrite) NSStringEncoding primaryEncoding; @property (nonatomic, assign, readwrite) NSTimeInterval lastMessageServerTime; @property (nonatomic, assign, readwrite) NSUInteger floodControlDelayTimerInterval; @property (nonatomic, assign, readwrite) NSUInteger floodControlMaximumMessages; @property (nonatomic, assign, readwrite) uint16_t proxyPort; @property (nonatomic, copy, readwrite) NSArray *channelList; @property (nonatomic, copy, readwrite) NSArray *highlightList; @property (nonatomic, copy, readwrite) NSArray *ignoreList; @property (nonatomic, copy, readwrite) NSArray *alternateNicknames; @property (nonatomic, copy, readwrite) NSArray *loginCommands; @property (nonatomic, copy, readwrite) NSArray *serverList; @property (nonatomic, copy, readwrite) NSString *connectionName; @property (nonatomic, copy, readwrite) NSString *nickname; @property (nonatomic, copy, readwrite) NSString *normalLeavingComment; @property (nonatomic, copy, readwrite) NSString *realName; @property (nonatomic, copy, readwrite) NSString *sleepModeLeavingComment; @property (nonatomic, copy, readwrite) NSString *username; @property (nonatomic, copy, readwrite, nullable) NSData *identityClientSideCertificate; @property (nonatomic, copy, readwrite, nullable) NSString *awayNickname; @property (nonatomic, copy, readwrite, nullable) NSString *nicknamePassword; @property (nonatomic, copy, readwrite, nullable) NSString *proxyAddress; @property (nonatomic, copy, readwrite, nullable) NSString *proxyPassword; @property (nonatomic, copy, readwrite, nullable) NSString *proxyUsername; @property (nonatomic, assign, readwrite) RCMCipherSuiteCollection cipherSuites; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCColorFormat.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, IRCTextFormatterEffectType) { IRCTextFormatterEffectNone = 0, IRCTextFormatterEffectBold, IRCTextFormatterEffectItalic, IRCTextFormatterEffectMonospace, IRCTextFormatterEffectStrikethrough, IRCTextFormatterEffectUnderline, IRCTextFormatterEffectForegroundColor, IRCTextFormatterEffectBackgroundColor, IRCTextFormatterEffectSpoiler, }; typedef NSString *IRCTextFormatterAttributeName NS_EXTENSIBLE_STRING_ENUM; TEXTUAL_EXTERN IRCTextFormatterAttributeName const IRCTextFormatterBoldAttributeName; // BOOL TEXTUAL_EXTERN IRCTextFormatterAttributeName const IRCTextFormatterItalicAttributeName; // BOOL TEXTUAL_EXTERN IRCTextFormatterAttributeName const IRCTextFormatterMonospaceAttributeName; // BOOL TEXTUAL_EXTERN IRCTextFormatterAttributeName const IRCTextFormatterStrikethroughAttributeName; // BOOL TEXTUAL_EXTERN IRCTextFormatterAttributeName const IRCTextFormatterUnderlineAttributeName; // BOOL TEXTUAL_EXTERN IRCTextFormatterAttributeName const IRCTextFormatterForegroundColorAttributeName; // NSNumber, 0-15 - or, NSColor TEXTUAL_EXTERN IRCTextFormatterAttributeName const IRCTextFormatterBackgroundColorAttributeName; // NSNumber, 0-15 - or, NSColor TEXTUAL_EXTERN IRCTextFormatterAttributeName const IRCTextFormatterSpoilerAttributeName; // BOOL #define IRCTextFormatterEffectColorAsDigitCharacter 0x03 #define IRCTextFormatterEffectColorAsHexCharacter 0x04 #define IRCTextFormatterEffectBoldCharacter 0x02 #define IRCTextFormatterEffectItalicCharacter 0x1d #define IRCTextFormatterEffectItalicCharacterOld 0x16 #define IRCTextFormatterEffectMonospaceCharacter 0x11 #define IRCTextFormatterEffectStrikethroughCharacter 0x1e #define IRCTextFormatterEffectUnderlineCharacter 0x1F #define IRCTextFormatterTerminatingCharacter 0x0F #define IRCTextFormatterEffectColorHighestDigit 98 @class IRCTextFormatterEffects; @interface IRCTextFormatterEffect : NSObject @property (readonly) IRCTextFormatterEffectType type; @property (readonly, copy, nullable) NSString *value; @property (readonly) UniChar controlCharacter; /* Number of bytes needed to support this effect. Open control character + value + close control character. For background color, only the comma and color value is counted. */ @property (readonly) NSUInteger length; + (nullable instancetype)effectWithType:(IRCTextFormatterEffectType)type; + (nullable instancetype)effectWithType:(IRCTextFormatterEffectType)type withValue:(nullable id)value; - (nullable instancetype)initWithEffect:(IRCTextFormatterEffectType)type; - (nullable instancetype)initWithEffect:(IRCTextFormatterEffectType)type withValue:(nullable id)value NS_DESIGNATED_INITIALIZER; /* Appends control character and value for the effect. For background color, appends comma and color value instead. */ - (void)appendToStartOf:(NSMutableString *)string; /* Appends control character for the effect. For background color, does nothing. */ - (void)appendToEndOf:(NSMutableString *)string; @end @interface IRCTextFormatterEffects : NSObject @property (readonly, copy) NSArray *effects; /* Number of bytes needed to support all effects. */ @property (readonly) NSUInteger maximumLength; + (instancetype)effectsInAttributes:(NSDictionary *)attributes; - (instancetype)initWithAttributes:(NSDictionary *)attributes NS_DESIGNATED_INITIALIZER; - (void)appendToStartOf:(NSMutableString *)string; - (void)appendToEndOf:(NSMutableString *)string; @end #pragma mark - @interface NSAttributedString (IRCTextFormatter) - (BOOL)IRCFormatterAttributeSetInRange:(IRCTextFormatterEffectType)effect range:(NSRange)limitRange; /* Returns an NSString with appropriate formatting characters. */ @property (readonly, copy) NSString *stringFormattedForIRC; @end #pragma mark - @interface NSMutableAttributedString (IRCTextFormatter) - (void)setIRCFormatterAttribute:(IRCTextFormatterEffectType)effect value:(id)value range:(NSRange)limitRange; - (void)removeIRCFormatterAttribute:(IRCTextFormatterEffectType)effect range:(NSRange)limitRange; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCCommandIndex.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN /* Local commands are client-local commands */ typedef NS_ENUM(NSUInteger, IRCLocalCommand) { IRCLocalCommandAdchat = 5001, IRCLocalCommandAme = 5002, IRCLocalCommandAmsg = 5003, IRCLocalCommandAquote = 5095, IRCLocalCommandAraw = 5096, IRCLocalCommandAutojoin = 5101, IRCLocalCommandAway = 5004, IRCLocalCommandBack = 5105, IRCLocalCommandBan = 5005, IRCLocalCommandCap = 5006, IRCLocalCommandCaps = 5007, IRCLocalCommandChatops = 5009, IRCLocalCommandClear = 5010, IRCLocalCommandClearall = 5011, IRCLocalCommandClose = 5012, IRCLocalCommandConn = 5013, IRCLocalCommandCtcp = 5014, IRCLocalCommandCtcpreply = 5015, IRCLocalCommandCycle = 5016, IRCLocalCommandDcc = 5017, IRCLocalCommandDebug = 5018, IRCLocalCommandDefaults = 5092, IRCLocalCommandDehalfop = 5019, IRCLocalCommandDeop = 5020, IRCLocalCommandDevoice = 5021, IRCLocalCommandEcho = 5022, IRCLocalCommandGetscripts = 5098, IRCLocalCommandGline = 5023, IRCLocalCommandGlobops = 5024, IRCLocalCommandGoto = 5099, IRCLocalCommandGzline = 5025, IRCLocalCommandHalfop = 5026, IRCLocalCommandHop = 5027, IRCLocalCommandIgnore = 5029, IRCLocalCommandInvite = 5030, IRCLocalCommandIson = 5100, IRCLocalCommandJ = 5031, IRCLocalCommandJoin = 5032, IRCLocalCommandJoinRandom = 5109, IRCLocalCommandKb = 5083, IRCLocalCommandKick = 5033, IRCLocalCommandKickban = 5034, IRCLocalCommandKill = 5035, IRCLocalCommandLagcheck = 5084, IRCLocalCommandLeave = 5036, IRCLocalCommandList = 5037, IRCLocalCommandLocops = 5039, IRCLocalCommandM = 5040, IRCLocalCommandMe = 5041, IRCLocalCommandMode = 5042, IRCLocalCommandMonitor = 5106, IRCLocalCommandMsg = 5043, IRCLocalCommandMute = 5044, IRCLocalCommandMylag = 5045, IRCLocalCommandMyversion = 5046, IRCLocalCommandNachat = 5047, IRCLocalCommandNames = 5094, IRCLocalCommandNick = 5048, IRCLocalCommandNotice = 5050, IRCLocalCommandNotifybubble = 5112, IRCLocalCommandNotifysound = 5113, IRCLocalCommandNotifyspeak = 5114, IRCLocalCommandOmsg = 5051, IRCLocalCommandOnotice = 5052, IRCLocalCommandOp = 5053, IRCLocalCommandPart = 5054, IRCLocalCommandPass = 5055, IRCLocalCommandQuery = 5056, IRCLocalCommandQuiet = 5107, IRCLocalCommandQuit = 5057, IRCLocalCommandQuote = 5058, IRCLocalCommandRaw = 5059, IRCLocalCommandRecv = 5087, IRCLocalCommandRejoin = 5060, IRCLocalCommandRemove = 5061, IRCLocalCommandServer = 5062, IRCLocalCommandSetcolor = 5103, IRCLocalCommandSetqueryname = 5117, IRCLocalCommandShun = 5063, IRCLocalCommandSme = 5064, IRCLocalCommandSmsg = 5065, IRCLocalCommandSslcontext = 5066, IRCLocalCommandT = 5067, IRCLocalCommandTage = 5093, IRCLocalCommandTempshun = 5068, IRCLocalCommandTimer = 5069, IRCLocalCommandTopic = 5070, IRCLocalCommandUme = 5089, IRCLocalCommandUmode = 5071, IRCLocalCommandUmsg = 5088, IRCLocalCommandUnban = 5072, IRCLocalCommandUnignore = 5073, IRCLocalCommandUnmute = 5075, IRCLocalCommandUnotice = 5090, IRCLocalCommandUnquiet = 5108, IRCLocalCommandVoice = 5076, IRCLocalCommandWallops = 5077, IRCLocalCommandWatch = 5097, IRCLocalCommandWeights = 5118, IRCLocalCommandWho = 5079, IRCLocalCommandWhois = 5080, IRCLocalCommandWhowas = 5081, IRCLocalCommandZline = 5082 }; /* Remote commands are server-side commands */ typedef NS_ENUM(NSUInteger, IRCRemoteCommand) { IRCRemoteCommandAdchat = 1003, IRCRemoteCommandAuthenticate = 1005, IRCRemoteCommandAway = 1050, IRCRemoteCommandBatch = 1054, IRCRemoteCommandCap = 1004, IRCRemoteCommandCertinfo = 1055, IRCRemoteCommandChatops = 1006, IRCRemoteCommandChghost = 1057, IRCRemoteCommandError = 1016, IRCRemoteCommandGline = 1047, IRCRemoteCommandGlobops = 1017, IRCRemoteCommandGzline = 1048, IRCRemoteCommandInvite = 1018, IRCRemoteCommandIson = 1019, IRCRemoteCommandJoin = 1020, IRCRemoteCommandKick = 1021, IRCRemoteCommandKill = 1022, IRCRemoteCommandList = 1023, IRCRemoteCommandLocops = 1024, IRCRemoteCommandMode = 1026, IRCRemoteCommandMonitor = 1056, IRCRemoteCommandNachat = 1027, IRCRemoteCommandNames = 1028, IRCRemoteCommandNick = 1029, IRCRemoteCommandNotice = 1030, IRCRemoteCommandPart = 1031, IRCRemoteCommandPass = 1032, IRCRemoteCommandPing = 1033, IRCRemoteCommandPong = 1034, IRCRemoteCommandPrivmsg = 1035, IRCRemoteCommandPrivmsgAction = 1002, IRCRemoteCommandQuit = 1036, IRCRemoteCommandShun = 1045, IRCRemoteCommandTempshun = 1046, IRCRemoteCommandTime = 1012, IRCRemoteCommandTopic = 1039, IRCRemoteCommandUser = 1037, IRCRemoteCommandWallops = 1038, IRCRemoteCommandWatch = 1053, IRCRemoteCommandWho = 1040, IRCRemoteCommandWhois = 1042, IRCRemoteCommandWhowas = 1041, IRCRemoteCommandZline = 1049 }; /* Controlling class */ @interface IRCCommandIndex : NSObject + (NSArray *)localCommandList; + (NSUInteger)indexOfRemoteCommand:(NSString *)command; + (NSUInteger)indexOfLocalCommand:(NSString *)command; + (NSUInteger)colonPositionForRemoteCommand:(NSString *)command; + (nullable NSString *)syntaxForLocalCommand:(NSString *)command; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCConnection.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCConnectionConfig; @interface IRCConnection : NSObject @property (readonly, weak) IRCClient *client; @property (readonly, copy) IRCConnectionConfig *config; @property (readonly) BOOL isConnected; @property (readonly) BOOL isConnectedWithClientSideCertificate; @property (readonly) BOOL isConnecting; @property (readonly) BOOL isDisconnecting; @property (readonly) BOOL isSecured; @property (readonly) BOOL isSending; @property (readonly) BOOL EOFReceived; @property (readonly, nullable) NSString *connectedAddress; // nil if connected to a proxy @property (readonly, copy) NSString *uniqueIdentifier; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfig:(IRCConnectionConfig *)config onClient:(IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)open; - (void)close; - (void)sendLine:(NSString *)line; - (void)clearSendQueue; @end @protocol IRCConnectionDelegate @required - (void)ircConnection:(IRCConnection *)sender willConnectToProxy:(NSString *)proxyHost port:(uint16_t)proxyPort; - (void)ircConnectionDidConnect:(IRCConnection *)sender; - (void)ircConnectionDidSecureConnection:(IRCConnection *)sender withProtocolType:(tls_protocol_version_t)protocolType cipherSuite:(tls_ciphersuite_t)cipherSuite; - (void)ircConnectionDidCloseReadStream:(IRCConnection *)sender; - (void)ircConnection:(IRCConnection *)sender didDisconnectWithError:(nullable NSError *)disconnectError; - (void)ircConnection:(IRCConnection *)sender didReceiveData:(NSString *)data; - (void)ircConnection:(IRCConnection *)sender willSendData:(NSString *)data; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCHighlightLogEntry.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCLogLine; @interface IRCHighlightLogEntry : XRPortablePropertyObject @property (readonly, copy) TVCLogLine *lineLogged; @property (readonly, copy) NSDate *timeLogged; @property (readonly, copy) NSString *clientId; @property (readonly, copy) NSString *channelId; @property (readonly, copy) NSString *lineNumber; @property (readonly, copy) NSAttributedString *renderedMessage; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCHighlightMatchCondition.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Immutable Object @interface IRCHighlightMatchCondition : XRPortablePropertyDict @property (readonly, copy) NSString *uniqueIdentifier; @property (readonly, copy) NSString *matchKeyword; @property (readonly, copy, nullable) NSString *matchChannelId; @property (readonly) BOOL matchIsExcluded; @end #pragma mark - #pragma mark Mutable Object @interface IRCHighlightMatchConditionMutable : IRCHighlightMatchCondition @property (nonatomic, copy, readwrite) NSString *matchKeyword; @property (nonatomic, copy, readwrite, nullable) NSString *matchChannelId; @property (nonatomic, assign, readwrite) BOOL matchIsExcluded; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCISupportInfo.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCModeInfo; typedef NS_ENUM(NSUInteger, IRCISupportInfoListType) { IRCISupportInfoListTypeBan, IRCISupportInfoListTypeBanException, IRCISupportInfoListTypeInviteException, IRCISupportInfoListTypeQuiet }; #define IRCISupportInfoHighestUserPrefixRank 100 #define IRCISupportUserModeSymbolsSymbolsKey @"modeSymbols" #define IRCISupportUserModeSymbolsCharactersKey @"characters" @interface IRCISupportInfo : NSObject @property (readonly) BOOL configurationReceived; @property (readonly) NSUInteger maximumAwayLength; // 0 = no limit @property (readonly) NSUInteger maximumChannelNameLength; // 0 = no limit - unused @property (readonly) NSUInteger maximumKeyLength; // 0 = no limit @property (readonly) NSUInteger maximumKickLength; // 0 = no limit @property (readonly) NSUInteger maximumNicknameLength; @property (readonly) NSUInteger maximumTopicLength; // 0 = no limit @property (readonly) NSUInteger maximumModeCount; @property (readonly, copy) NSArray *channelNamePrefixes; @property (readonly, copy) NSArray *statusMessageModeSymbols; @property (readonly, copy) NSDictionary *channelModes; @property (readonly, copy) NSDictionary *userModeSymbols; @property (readonly, copy, nullable) NSString *banExceptionModeSymbol; @property (readonly, copy, nullable) NSString *inviteExceptionModeSymbol; @property (readonly, copy, nullable) NSString *serverAddress; @property (readonly, copy, nullable) NSString *networkName; @property (readonly, copy, nullable) NSString *networkNameFormatted; - (instancetype)init NS_UNAVAILABLE; - (nullable NSString *)modeSymbolForUserPrefix:(NSString *)character; - (nullable NSString *)userPrefixForModeSymbol:(NSString *)modeSymbol; - (BOOL)characterIsUserPrefix:(NSString *)character; - (BOOL)modeSymbolIsUserPrefix:(NSString *)modeSymbol; - (nullable NSString *)statusMessagePrefixForModeSymbol:(NSString *)modeSymbol; - (NSString *)extractStatusMessagePrefixFromChannelNamed:(NSString *)channel; - (NSUInteger)rankForUserPrefixWithMode:(NSString *)modeSymbol; // Starts at 100; 100 = highest rank - (IRCModeInfo *)createModeWithSymbol:(NSString *)modeSymbol; - (IRCModeInfo *)createModeWithSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameter:(nullable NSString *)modeParameter; - (BOOL)isListSupported:(IRCISupportInfoListType)listType; - (nullable NSString *)modeSymbolForList:(IRCISupportInfoListType)listType; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCMessage.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCPrefix; #pragma mark - #pragma mark Immutable Object @interface IRCMessage : XRPortablePropertyObject @property (readonly, copy) IRCPrefix *sender; @property (readonly, copy) NSString *command; @property (readonly) NSUInteger commandNumeric; @property (readonly, copy) NSArray *params; @property (readonly, copy) NSDate *receivedAt; @property (readonly) BOOL isHistoric; // Whether a custom @time= was supplied during parsing. @property (readonly) BOOL isEventOnlyMessage; /* The message should be parsed and special actions performed such as adding/removing user but the result is never passed to print: */ @property (readonly) BOOL isPrintOnlyMessage; /* The message should be parsed and passed to print: but special actions such as adding/removing user from member list should be ignored. (currently unused) */ @property (readonly, copy, nullable) NSString *batchToken; @property (readonly, copy, nullable) NSDictionary *messageTags; /* IRCv3 message tags. See ircv3.net for more information regarding extensions in the IRC protocol. */ - (nullable instancetype)initWithLine:(NSString *)line; - (nullable instancetype)initWithLine:(NSString *)line onClient:(nullable IRCClient *)client NS_DESIGNATED_INITIALIZER; @property (readonly, copy, nullable) NSString *senderNickname; @property (readonly, copy, nullable) NSString *senderUsername; @property (readonly, copy, nullable) NSString *senderAddress; @property (readonly, copy, nullable) NSString *senderHostmask; @property (readonly) BOOL senderIsServer; @property (readonly) NSUInteger paramsCount; - (NSString *)paramAt:(NSUInteger)index; @property (readonly, copy) NSString *sequence; - (NSString *)sequence:(NSUInteger)index; @end #pragma mark - #pragma mark Mutable Object @interface IRCMessageMutable : IRCMessage @property (nonatomic, copy, readwrite) IRCPrefix *sender; @property (nonatomic, copy, readwrite) NSString *command; @property (nonatomic, assign, readwrite) NSUInteger commandNumeric; @property (nonatomic, copy, readwrite) NSArray *params; @property (nonatomic, copy, readwrite) NSDate *receivedAt; @property (nonatomic, assign, readwrite) BOOL isHistoric; @property (nonatomic, assign, readwrite) BOOL isEventOnlyMessage; @property (nonatomic, assign, readwrite) BOOL isPrintOnlyMessage; @property (nonatomic, copy, readwrite, nullable) NSString *batchToken; @property (nonatomic, copy, readwrite, nullable) NSDictionary *messageTags; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCModeInfo.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient; #pragma mark - #pragma mark Immutable Object @interface IRCModeInfo : XRPortablePropertyObject @property (readonly) BOOL modeIsSet; @property (readonly, copy) NSString *modeSymbol; @property (readonly, copy, nullable) NSString *modeParameter; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithModeSymbol:(NSString *)modeSymbol; - (instancetype)initWithModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet; - (instancetype)initWithModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameter:(nullable NSString *)modeParameter NS_DESIGNATED_INITIALIZER; - (BOOL)isModeForChangingMemberModeOn:(IRCClient *)client; @end #pragma mark - #pragma mark Mutable Object @interface IRCModeInfoMutable : IRCModeInfo @property (nonatomic, assign, readwrite) BOOL modeIsSet; @property (nonatomic, copy, readwrite) NSString *modeSymbol; @property (nonatomic, copy, readwrite, nullable) NSString *modeParameter; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCNetworkList.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCNetwork; @interface IRCNetworkList : NSObject @property (readonly, copy) NSArray *listOfNetworks; - (nullable IRCNetwork *)networkNamed:(NSString *)networkName; - (nullable IRCNetwork *)networkWithServerAddress:(NSString *)serverAddress; @end #pragma mark - @interface IRCNetwork : NSObject @property (readonly, copy) NSString *networkName; @property (readonly, copy) NSString *networkDescription NS_UNAVAILABLE; // unused @property (readonly, copy) NSString *serverAddress; @property (readonly) uint16_t serverPort; @property (readonly) BOOL prefersSecuredConnection; - (instancetype)init NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCNumerics.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ enum { /* Replies */ RPL_WELCOME = 1, RPL_YOURHOST = 2, RPL_CREATED = 3, RPL_MYINFO = 4, RPL_ISUPPORT = 5, RPL_REDIR = 10, RPL_UMODEIS = 221, RPL_STATSCONN = 250, RPL_LUSERCLIENT = 251, RPL_LUSERHOP = 252, RPL_LUSERUNKNOWN = 253, RPL_LUSERCHANNELS = 254, RPL_LUSERME = 255, RPL_LOCALUSERS = 265, RPL_GLOBALUSERS = 266, RPL_WHOISCERTFP = 276, RPL_AWAY = 301, RPL_ISON = 303, RPL_UNAWAY = 305, RPL_NOWAWAY = 306, RPL_WHOISREGNICK = 307, RPL_WHOISHELPOP = 310, RPL_WHOISUSER = 311, RPL_WHOISSERVER = 312, RPL_WHOISOPERATOR = 313, RPL_WHOWASUSER = 314, RPL_ENDOFWHO = 315, RPL_WHOISIDLE = 317, RPL_ENDOFWHOIS = 318, RPL_WHOISCHANNELS = 319, RPL_WHOISSPECIAL = 320, RPL_LISTSTART = 321, RPL_LIST = 322, RPL_LISTEND = 323, RPL_CHANNELMODEIS = 324, RPL_CHANNEL_URL = 328, RPL_CREATIONTIME = 329, RPL_WHOISACCOUNT = 330, RPL_TOPIC = 332, RPL_TOPICWHOTIME = 333, RPL_WHOISBOT = 335, RPL_WHOISACTUALLY = 338, RPL_INVITING = 341, RPL_INVITELIST = 346, RPL_ENDOFINVITELIST = 347, RPL_EXCEPTLIST = 348, RPL_ENDOFEXCEPTLIST = 349, RPL_WHOREPLY = 352, RPL_NAMEREPLY = 353, RPL_ENDOFNAMES = 366, RPL_BANLIST = 367, RPL_ENDOFBANLIST = 368, RPL_ENDOFWHOWAS = 369, RPL_MOTD = 372, RPL_MOTDSTART = 375, RPL_ENDOFMOTD = 376, RPL_WHOISHOST = 378, RPL_WHOISMODES = 379, RPL_YOUREOPER = 381, RPL_REAWAY = 597, RPL_GONEAWAY = 598, RPL_NOTAWAY = 599, RPL_LOGON = 600, RPL_LOGOFF = 601, RPL_WATCHOFF = 602, RPL_WATCHSTAT = 603, RPL_NOWON = 604, RPL_NOWOFF = 605, RPL_WATCHLIST = 606, RPL_ENDOFWATCHLIST = 607, RPL_CLEARWATCH = 608, /* RPL_CHANNELSMSG and RPL_WHOWASIP were added recently. Reference: https://github.com/inspircd/inspircd/commit/150258b1f110aad58c8882b76474a4ceb3b2ab97 */ RPL_CHANNELSMSG = 651, // (is on private/secret channels..., InspIRCd) RPL_WHOWASIP = 652, // (was connected from..., InspIRCd) RPL_WHOISSECURE = 671, RPL_WHOISREALIP = 672, // (is a CGI:IRC client from..., Rizon) RPL_TARGUMODEG = 716, RPL_TARGNOTIFY = 717, RPL_UMODEGMSG = 718, RPL_QUIETLIST = 728, RPL_ENDOFQUIETLIST = 729, RPL_MONONLINE = 730, RPL_MONOFFLINE = 731, RPL_MONLIST = 732, RPL_ENDOFMONLIST = 733, RPL_LOGGEDIN = 900, RPL_LOGGEDOUT = 901, RPL_SASLSUCCESS = 903, RPL_SASLMECHS = 908, /* Errors */ ERR_NOSUCHNICK = 401, ERR_NOSUCHSERVER = 402, ERR_NOSUCHCHANNEL = 403, ERR_CANNOTSENDTOCHAN = 404, ERR_TOOMANYCHANNELS = 405, ERR_UNKNOWNCOMMAND = 421, ERR_NOMOTD = 422, ERR_ERRONEUSNICKNAME = 432, ERR_NICKNAMEINUSE = 433, ERR_BANNICKCHANGE = 435, ERR_UNAVAILRESOURCE = 437, ERR_NICKTOOFAST = 438, ERR_CANTCHANGENICK = 447, ERR_FORBIDDENCHANNEL = 448, ERR_NOHIDING = 459, ERR_NEEDMOREPARAMS = 461, ERR_LINKCHANNEL = 470, ERR_CHANNELISFULL = 471, ERR_INVITEONLYCHAN = 473, ERR_BANNEDFROMCHAN = 474, ERR_BADCHANNELKEY = 475, ERR_BADCHANMASK = 476, ERR_NEEDREGGEDNICK = 477, ERR_BADCHANNAME = 479, ERR_THROTTLE = 480, ERR_SECUREONLYCHAN = 489, ERR_DELAYREJOIN = 495, ERR_TOOMANYJOINS = 500, ERR_TOOMANYWATCH = 512, ERR_DISABLED = 517, ERR_ADMONLY = 519, ERR_OPERONLY = 520, ERR_WHOSYNTAX = 522, ERR_WHOLIMEXCEED = 523, ERR_OPERSPVERIFY = 524, ERR_MONLISTFULL = 734, ERR_NICKLOCKED = 902, ERR_SASLFAIL = 904, ERR_SASLTOOLONG = 905, ERR_SASLABORTED = 906, ERR_SASLALREADY = 907, ERR_BADCHANNEL = 926 }; ================================================ FILE: Sources/App/Classes/Headers/IRCPrefix.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Immutable Object @interface IRCPrefix : XRPortablePropertyObject @property (readonly) BOOL isServer; @property (readonly, copy) NSString *hostmask; // Defaults to empty string @property (readonly, copy) NSString *nickname; // Defaults to empty string @property (readonly, copy, nullable) NSString *username; @property (readonly, copy, nullable) NSString *address; @end #pragma mark - #pragma mark Mutable Object @interface IRCPrefixMutable : IRCPrefix @property (nonatomic, assign, readwrite) BOOL isServer; @property (nonatomic, copy, readwrite) NSString *hostmask; @property (nonatomic, copy, readwrite) NSString *nickname; @property (nonatomic, copy, readwrite, nullable) NSString *username; @property (nonatomic, copy, readwrite, nullable) NSString *address; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCSendingMessage.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface IRCSendingMessage : NSObject + (NSString *)stringWithCommand:(NSString *)command arguments:(nullable NSArray *)arguments; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCServer.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Immutable Object @interface IRCServer : XRPortablePropertyDict @property (readonly, copy) NSString *uniqueIdentifier; @property (readonly, copy) NSString *serverAddress; @property (readonly, copy, nullable) NSString *serverPassword; @property (readonly, copy, nullable) NSString *serverPasswordFromKeychain; @property (readonly) uint16_t serverPort; @property (readonly) BOOL prefersSecuredConnection; @end #pragma mark - #pragma mark Mutable Object @interface IRCServerMutable : IRCServer @property (nonatomic, copy, readwrite) NSString *serverAddress; @property (nonatomic, copy, readwrite, nullable) NSString *serverPassword; @property (nonatomic, assign, readwrite) uint16_t serverPort; @property (nonatomic, assign, readwrite) BOOL prefersSecuredConnection; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCTreeItem.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel, TVCLogController; @interface IRCTreeItem : NSObject @property (readonly) BOOL isActive; @property (readonly) BOOL isUnread; @property (readonly) BOOL isClient; @property (readonly) BOOL isChannel; @property (readonly) BOOL isPrivateMessage; @property (readonly, weak) IRCClient *associatedClient; @property (readonly, weak) IRCChannel *associatedChannel; @property (readonly) NSString *label; @property (readonly) NSString *name; @property (readonly) NSString *uniqueIdentifier; @property (readonly) NSUInteger dockUnreadCount; @property (readonly) NSUInteger nicknameHighlightCount; @property (readonly) NSUInteger treeUnreadCount; @property (readonly) TVCLogController *viewController; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCUser.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Each user on a server is allocated one instance of IRCUser. IRCUser is used to keep track of information related to the user. */ /* There is ever only one instance of IRCUser kept track of by the IRCClient class. It is possible to create a mutable copy of a user to change properties, but those changes will not be recognized until the modified user is given to IRCClient. IRCClient will then perform the actions necessary to update all components depending on the user. */ NS_ASSUME_NONNULL_BEGIN @class IRCClient; #pragma mark - #pragma mark Immutable Object @interface IRCUser : XRPortablePropertyObject @property (readonly, copy) NSString *nickname; @property (readonly, copy, nullable) NSString *username; @property (readonly, copy, nullable) NSString *address; @property (readonly, copy, nullable) NSString *hostmask; @property (readonly, copy, nullable) NSString *hostmaskFragment; // -hostmask without nickname @property (readonly, copy, nullable) NSString *realName; @property (readonly) BOOL isAway; @property (readonly) BOOL isIRCop; @property (readonly, copy) NSString *banMask; @property (readonly, copy) NSString *lowercaseNickname; @property (readonly, copy) NSString *uppercaseNickname; /* -presentAwayMessageFor301 keeps track of the last time raw numeric 301 (away message) is received and will return YES if the message should be presented, NO otherwise. */ @property (readonly) BOOL presentAwayMessageFor301; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithNickname:(NSString *)nickname onClient:(IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)markAsAway; - (void)markAsReturned; @end #pragma mark - #pragma mark Mutable Object @interface IRCUserMutable : IRCUser @property (nonatomic, copy, readwrite) NSString *nickname; // Defaults to empty string @property (nonatomic, copy, readwrite, nullable) NSString *username; @property (nonatomic, copy, readwrite, nullable) NSString *address; @property (nonatomic, copy, readwrite, nullable) NSString *realName; @property (nonatomic, assign, readwrite) BOOL isAway; @property (nonatomic, assign, readwrite) BOOL isIRCop; - (instancetype)initWithClient:(IRCClient *)client; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCUserRelations.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCUser.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel, IRCChannelUser; @interface IRCUser (IRCUserRelations) @property (readonly, copy) NSDictionary *relations; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/IRCWorld.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCClientConfig, IRCChannel, IRCChannelConfig, IRCTreeItem; TEXTUAL_EXTERN NSString * const IRCWorldClientListDefaultsKey; TEXTUAL_EXTERN NSNotificationName const IRCWorldClientListWasModifiedNotification; TEXTUAL_EXTERN NSNotificationName const IRCWorldDateHasChangedNotification; TEXTUAL_EXTERN NSNotificationName const IRCWorldWillDestroyClientNotification; TEXTUAL_EXTERN NSNotificationName const IRCWorldWillDestroyChannelNotification; @interface IRCWorld : NSObject @property (readonly) NSUInteger messagesSent; @property (readonly) NSUInteger messagesReceived; @property (readonly) uint64_t bandwidthIn; @property (readonly) uint64_t bandwidthOut; @property (readonly, copy) NSArray *clientList; @property (readonly) NSUInteger clientCount; - (void)save; - (void)savePeriodically; - (NSArray<__kindof IRCTreeItem *> *)findItemsWithIds:(NSArray *)itemIds; - (nullable IRCTreeItem *)findItemWithId:(NSString *)itemId; - (nullable IRCClient *)findClientWithId:(NSString *)clientId; - (nullable IRCChannel *)findChannelWithId:(NSString *)channelId onClientWithId:(NSString *)clientId; - (nullable IRCClient *)findClientWithServerAddress:(NSString *)serverAddress; - (IRCClient *)createClientWithConfig:(IRCClientConfig *)config; - (IRCChannel *)createChannelWithConfig:(IRCChannelConfig *)config onClient:(IRCClient *)client; - (IRCChannel *)createPrivateMessage:(NSString *)nickname onClient:(IRCClient *)client; - (void)destroyClient:(IRCClient *)client; - (void)destroyChannel:(IRCChannel *)channel; - (void)evaluateFunctionOnAllViews:(NSString *)function arguments:(nullable NSArray *)arguments; // Defaults to onQueue YES - (void)evaluateFunctionOnAllViews:(NSString *)function arguments:(nullable NSArray *)arguments onQueue:(BOOL)onQueue; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCAddressBookInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCAddressBook.h" NS_ASSUME_NONNULL_BEGIN @interface IRCAddressBookEntry () { @protected BOOL _ignoreClientToClientProtocol; BOOL _ignoreFileTransferRequests; BOOL _ignoreGeneralEventMessages; BOOL _ignoreInlineMedia; BOOL _ignoreNoticeMessages; BOOL _ignorePrivateMessageHighlights; BOOL _ignorePrivateMessages; BOOL _ignorePublicMessageHighlights; BOOL _ignorePublicMessages; BOOL _trackUserActivity; IRCAddressBookEntryType _entryType; NSString *_hostmask; NSString *_hostmaskRegularExpression; NSString *_trackingNickname; NSArray *_parentEntries; @private NSString *_uniqueIdentifier; NSDictionary *_defaults; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCChannelConfigInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelConfigPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCChannelConfig () { @protected BOOL _autoJoin; BOOL _ignoreGeneralEventMessages; BOOL _ignoreHighlights; BOOL _inlineMediaDisabled; BOOL _inlineMediaEnabled; BOOL _pushNotifications; BOOL _showTreeBadgeCount; IRCChannelType _type; NSString *_channelName; NSString *_label; NSString *_defaultModes; NSString *_defaultTopic; NSString *_secretKey; NSMutableDictionary *_notificationsMutable; @private NSString *_uniqueIdentifier; NSDictionary *_defaults; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCChannelUserInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelUserPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCChannelUser () { @protected NSString *_modes; double _incomingWeight; double _outgoingWeight; CFAbsoluteTime _lastWeightFade; NSTimeInterval _creationTime; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCClientConfigInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClientConfigPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCClientConfig () { @protected BOOL _autoConnect; BOOL _autoReconnect; BOOL _autoSleepModeDisconnect; BOOL _autojoinWaitsForNickServ; BOOL _connectionPrefersIPv4; BOOL _hideAutojoinDelayedWarnings; BOOL _hideNetworkUnavailabilityNotices; BOOL _performDisconnectOnPongTimer; BOOL _performDisconnectOnReachabilityChange; BOOL _performPongTimer; BOOL _prefersSecuredConnection; BOOL _saslAuthenticationDisableExternalMechanism; BOOL _sendAuthenticationRequestsToUserServ; BOOL _sendWhoCommandRequestsToChannels; BOOL _setInvisibleModeOnConnect; BOOL _sidebarItemExpanded; BOOL _validateServerCertificateChain; BOOL _zncIgnoreConfiguredAutojoin; BOOL _zncIgnorePlaybackNotifications; BOOL _zncIgnoreUserNotifications; BOOL _zncOnlyPlaybackLatest; IRCConnectionAddressType _addressType; IRCConnectionProxyType _proxyType; NSArray *_ignoreList; NSArray *_channelList; NSArray *_highlightList; NSArray *_alternateNicknames; NSArray *_loginCommands; NSArray *_serverList; NSData *_identityClientSideCertificate; NSString *_awayNickname; NSString *_connectionName; NSString *_nickname; NSString *_nicknamePassword; NSString *_normalLeavingComment; NSString *_proxyAddress; NSString *_proxyPassword; NSString *_proxyUsername; NSString *_realName; NSString *_serverAddress; NSString *_sleepModeLeavingComment; NSString *_username; NSStringEncoding _fallbackEncoding; NSStringEncoding _primaryEncoding; NSTimeInterval _lastMessageServerTime; NSUInteger _floodControlDelayTimerInterval; NSUInteger _floodControlMaximumMessages; uint16_t _proxyPort; uint16_t _serverPort; RCMCipherSuiteCollection _cipherSuites; @private BOOL _migratedServerPasswordPendingDestroy; NSUInteger _dictionaryVersion; NSString *_uniqueIdentifier; NSDictionary *_defaults; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCHighlightLogEntryInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCHighlightLogEntryPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCHighlightLogEntry () { @protected TVCLogLine *_lineLogged; NSString *_clientId; NSString *_channelId; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCHighlightMatchConditionInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCHighlightMatchCondition.h" NS_ASSUME_NONNULL_BEGIN @interface IRCHighlightMatchCondition () { @protected BOOL _matchIsExcluded; NSString *_matchChannelId; NSString *_matchKeyword; @private NSString *_uniqueIdentifier; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCMessageInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCMessagePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCMessage () { @protected BOOL _isHistoric; BOOL _isEventOnlyMessage; BOOL _isPrintOnlyMessage; IRCPrefix *_sender; NSArray *_params; NSDate *_receivedAt; NSDictionary *_messageTags; NSString *_batchToken; NSString *_command; NSUInteger _commandNumeric; IRCMessageBatchMessage *_parentBatchMessage; } @end @interface IRCMessage (IRCMessageLineParser) - (BOOL)parseLine:(NSString *)line forClient:(nullable IRCClient *)client; - (void)parseExtensions:(NSString *)extensionInfo forClient:(nullable IRCClient *)client; - (void)parseSender:(NSString *)senderInfo forClient:(nullable IRCClient *)client; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCModeInfoInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCModeInfo.h" NS_ASSUME_NONNULL_BEGIN @interface IRCModeInfo () { @protected BOOL _modeIsSet; NSString *_modeSymbol; NSString *_modeParameter; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCPrefixInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCPrefix.h" NS_ASSUME_NONNULL_BEGIN @interface IRCPrefix () { @protected BOOL _isServer; NSString *_nickname; NSString *_username; NSString *_address; NSString *_hostmask; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCServerInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCServerPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCServer () { @protected BOOL _prefersSecuredConnection; NSString *_serverAddress; NSString *_serverPassword; uint16_t _serverPort; @private NSString *_uniqueIdentifier; NSDictionary *_defaults; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/IRCUserInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCUserPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCUser () { @protected NSString *_nickname; NSString *_username; NSString *_address; NSString *_realName; BOOL _isAway; BOOL _isIRCop; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/TDCChannelPropertiesSheetInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCChannelPropertiesSheetPrivate.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannelConfigMutable; @interface TDCChannelPropertiesSheet () @property (nonatomic, strong) IRCChannelConfigMutable *config; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/TDCChannelSpotlightAppearanceInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCChannelSpotlightAppearancePrivate.h" NS_ASSUME_NONNULL_BEGIN @class TDCChannelSpotlightPanel; @interface TDCChannelSpotlightAppearance () - (nullable instancetype)initWithWindow:(TDCChannelSpotlightPanel *)window; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/TDCChannelSpotlightControllerInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCChannelSpotlightControllerPrivate.h" NS_ASSUME_NONNULL_BEGIN @class TDCChannelSpotlightAppearance, TDCChannelSpotlightSearchResult; @interface TDCChannelSpotlightController () - (TDCChannelSpotlightAppearance *)userInterfaceObjects; @property (readonly, copy) NSString *searchString; @property (readonly) NSArray *searchResults; @property (readonly) NSArray *searchResultsFiltered; @property (readonly) NSUInteger searchResultsCount; @property (readonly) NSInteger selectedSearchResult; // -1 = none selected @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/TDCFileTransferDialogInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCFileTransferDialogPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCFileTransferDialog () @property (nonatomic, copy, readwrite, nullable) NSString *IPAddress; - (void)updateClearButton; - (void)updateMaintenanceTimer; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Internal/TVCLogLineInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCLogLinePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCLogLine () { @protected BOOL _isEncrypted; BOOL _isFirstForDay; BOOL _nicknameColorStyleOverride; NSArray *_excludeKeywords; NSArray *_highlightKeywords; NSDictionary *_rendererAttributes; NSDate *_receivedAt; NSString *_command; NSString *_messageBody; NSString *_nickname; NSString *_nicknameColorStyle; TVCLogLineMemberType _memberType; TVCLogLineType _lineType; NSUInteger _sessionIdentifier; @private NSString *_uniqueIdentifier; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/NSColorHelper.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface NSColor (TXColorHelper) @property (class, readonly, copy) NSArray *formatterColors; @property (class, readonly) NSColor *formatterWhiteColor; @property (class, readonly) NSColor *formatterBlackColor; @property (class, readonly) NSColor *formatterNavyBlueColor; @property (class, readonly) NSColor *formatterDarkGreenColor; @property (class, readonly) NSColor *formatterRedColor; @property (class, readonly) NSColor *formatterBrownColor; @property (class, readonly) NSColor *formatterPurpleColor; @property (class, readonly) NSColor *formatterOrangeColor; @property (class, readonly) NSColor *formatterYellowColor; @property (class, readonly) NSColor *formatterLimeGreenColor; @property (class, readonly) NSColor *formatterTealColor; @property (class, readonly) NSColor *formatterAquaCyanColor; @property (class, readonly) NSColor *formatterLightBlueColor; @property (class, readonly) NSColor *formatterFuchsiaPinkColor; @property (class, readonly) NSColor *formatterNormalGrayColor; @property (class, readonly) NSColor *formatterLightGrayColor; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/NSStringHelper.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient; TEXTUAL_EXTERN NSStringEncoding const TXDefaultPrimaryStringEncoding; TEXTUAL_EXTERN NSStringEncoding const TXDefaultFallbackStringEncoding; @interface NSString (TXStringHelper) @property (readonly, copy) NSString *stringByAppendingIRCFormattingStop; @property (readonly, copy, nullable) NSString *channelNameWithoutBang; // "#channel" -> "channel", "##channel" -> "#channel" - (nullable NSString *)channelNameWithoutBangOn:(IRCClient *)client; @property (readonly, copy, nullable) NSString *nicknameFromHostmask; @property (readonly, copy, nullable) NSString *usernameFromHostmask; @property (readonly, copy, nullable) NSString *addressFromHostmask; - (nullable NSAttributedString *)attributedStringWithIRCFormatting:(NSFont *)preferredFont preferredFontColor:(nullable NSColor *)preferredFontColor; - (nullable NSAttributedString *)attributedStringWithIRCFormatting:(NSFont *)preferredFont preferredFontColor:(nullable NSColor *)preferredFontColor honorFormattingPreference:(BOOL)formattingPreference; @property (readonly, copy) NSString *stripIRCEffects; @property (getter=isValidInternetAddress, readonly) BOOL validInternetAddress; @property (getter=isValidInternetPort, readonly) BOOL validInternetPort; @property (getter=isHostmask, readonly) BOOL hostmask; @property (getter=isIPv4Address, readonly) BOOL IPv4Address; @property (getter=isIPv6Address, readonly) BOOL IPv6Address; @property (getter=isIPAddress, readonly) BOOL IPAddress; - (BOOL)hostmaskComponents:(NSString * _Nullable * _Nullable)nickname username:(NSString * _Nullable * _Nullable)username address:(NSString * _Nullable * _Nullable)address; - (BOOL)hostmaskComponents:(NSString * _Nullable * _Nullable)nickname username:(NSString * _Nullable * _Nullable)username address:(NSString * _Nullable * _Nullable)address onClient:(nullable IRCClient *)client; @property (getter=isHostmaskNickname, readonly) BOOL hostmaskNickname; @property (getter=isHostmaskAddress, readonly) BOOL hostmaskAddress; @property (getter=isHostmaskUsername, readonly) BOOL hostmaskUsername; /* By handing a client to -isHostmask[*]On:, greater validity is promised as configuration options from ISUPPORT (005) and other contextual information can be accessed. */ - (BOOL)isHostmaskNicknameOn:(nullable IRCClient *)client; - (BOOL)isHostmaskUsernameOn:(nullable IRCClient *)client; - (BOOL)isHostmaskAddressOn:(nullable IRCClient *)client; @property (getter=isChannelName, readonly) BOOL channelName; - (BOOL)isChannelNameOn:(IRCClient *)client; // Client to parse CHANTYPES from @property (readonly, copy, nullable) NSString *stringWithValidURIScheme; - (NSArray *)base64EncodingWithLineLength:(NSUInteger)lineLength; - (NSUInteger)colorComponentsOfCharacter:(UniChar)character startingAt:(NSUInteger)rangeStart foregroundColor:(id _Nullable * _Nullable)foregroundColor backgroundColor:(id _Nullable * _Nullable)backgroundColor; - (nullable NSString *)padNicknameWithCharacter:(UniChar)padCharacter maximumLength:(NSUInteger)maximumLength; @property (readonly, copy, nullable) NSString *prettyLicenseKey; @property (readonly, copy) NSString *encodedMessageTagString; @property (readonly, copy) NSString *decodedMessageTagString; @property (getter=isModeSymbol, readonly) BOOL modeSymbol; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/NSViewHelper.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface NSWindow (TXWindowHelper) /* Reset size of window to accommodate -minSize */ - (void)changeFrameToMin; // display = YES; animate = NO - (void)changeFrameToMinAndDisplay:(BOOL)display; // animate = NO - (void)changeFrameToMinAndDisplay:(BOOL)display animate:(BOOL)animate; /* Sets content view to nil, resets frame to fit view, then assigns new view. */ - (void)replaceContentView:(NSView *)withView; @end @interface NSView (TXViewHelper) /* Self top, right, bottom, left will = superview with 0.0 constant. */ - (void)addConstraintsToSuperviewToHugEdges; /* Superview width and height will = self with 0.0 constant. */ /* A priority of 550 is used to encourage hugging. */ - (void)addConstraintsToSuperviewToEqualDimensions; /* Remove first subview (if one is present) and replaces it with subview. */ /* The new superview width and height will equal that of subview with a priority of 550. The subview top, right, bottom, and left will equal that of the superview with 0.0 constant. */ /* See -addConstraintsToSuperviewToHugEdges -addConstraintsToSuperviewToEqualDimensions */ - (void)replaceFirstSubview:(NSView *)withView; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/ICLPayloadLocalPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLPayload.h" NS_ASSUME_NONNULL_BEGIN @interface ICLPayload (ICLPayloadLocalPrivate) @property (copy, readonly) NSDictionary *javaScriptObject; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCAddressBookMatchCachePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCAddressBookEntry, IRCClient; @interface IRCAddressBookMatchCache : NSObject @property (readonly, weak) IRCClient *client; - (instancetype)initWithClient:(IRCClient *)client; /* If multiple address book entries exist for the same host, then we return all of them combined into a single instance. This object will have a blank hostmask and the entry type IRCAddressBookEntryTypeMixed. */ - (nullable IRCAddressBookEntry *)findAddressBookEntryForHostmask:(NSString *)hostmask; - (NSArray *)findIgnoresForHostmask:(NSString *)hostmask; - (void)clearCachedMatches; - (void)clearCachedMatchesForHostmask:(NSString *)hostmask; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCAddressBookUserTrackingPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCAddressBookUserTracking.h" NS_ASSUME_NONNULL_BEGIN @interface IRCAddressBookUserTrackingContainer () - (instancetype)initWithClient:(IRCClient *)client; - (IRCAddressBookUserTrackingStatus)_statusOfUser:(NSString *)nickname; - (void)addTrackedUser:(NSString *)nickname; - (void)_addTrackedUser:(NSString *)nickname; - (void)removeTrackedUser:(NSString *)nickname; - (void)_removeTrackedUser:(NSString *)nickname; - (void)clearTrackedUsers; - (void)statusOfTrackedNickname:(NSString *)nickname changedTo:(IRCAddressBookUserTrackingStatus)newStatus; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCChannelConfigPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelConfig.h" NS_ASSUME_NONNULL_BEGIN @interface IRCChannelConfig () @property (readonly, copy) NSDictionary *notifications; - (void)writeSecretKeyToKeychain; - (void)destroySecretKeyKeychainItem; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCChannelMemberListControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCChannel; /* The member list controller is the controller bound to the view in the main window. IRCChannelMemberList, when assigned to it, will maintain a 1:1 relation to its internal list and the content of the controller. */ /* DO NOT modify the controller directly. Allow IRCChannelMemberList to do the work for you to maintain the integrity of the internal list and the content of the controller. */ @interface IRCChannelMemberListController : NSArrayController /* A controller can be assigned to only one channel or none. */ /* TVCMainWindow is responsible for assignment using TVCMemberList as a proxy. */ - (void)assignToChannel:(nullable IRCChannel *)channel; - (void)replaceContents:(NSArray *)contents; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCChannelMemberListPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelMemberList.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel, IRCChannelMemberListController; @protocol IRCChannelMemberListPrivatePrototype - (void)addMember:(IRCChannelUser *)member checkForDuplicates:(BOOL)checkForDuplicates; /* The replaceInAllChannels: flag should only be used in extreme cases because there is A LOT of overhead to setting it. Textual only does it when the user list is configured to sort IRCop at top and IRCop status changes. That change requires the user to be resorted in every channel they are in. Knowing which channels they are in is easy because of IRCUserRelations, but the actual process of finding where to sort them at is very expensive. */ - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2; - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 resort:(BOOL)resort; - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 resort:(BOOL)resort replaceInAllChannels:(BOOL)replaceInAllChannels; - (void)changeMember:(NSString *)nickname mode:(NSString *)mode value:(BOOL)value; - (void)resortMember:(IRCChannelUser *)member; - (void)clearMembers; - (NSData *)pasteboardDataForMembers:(NSArray *)members; + (BOOL)readNicknamesFromPasteboardData:(NSData *)pasteboardData withBlock:(void (NS_NOESCAPE ^)(IRCChannel *channel, NSArray *nicknames))callbackBlock; + (BOOL)readMembersFromPasteboardData:(NSData *)pasteboardData withBlock:(void (NS_NOESCAPE ^)(IRCChannel *channel, NSArray *members))callbackBlock; @end @interface IRCChannelMemberList () - (instancetype)initWithChannel:(IRCChannel *)channel NS_DESIGNATED_INITIALIZER; + (void)suspendMemberListSerialQueues; + (void)resumeMemberListSerialQueues; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCChannelModePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelMode.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel; @interface IRCChannelMode () - (instancetype)initWithChannel:(IRCChannel *)channel NS_DESIGNATED_INITIALIZER; - (NSArray *)updateModes:(NSString *)modeString; - (void)clear; @end #pragma mark - @interface IRCChannelModeContainer () - (void)applyModes:(NSArray *)modes; - (void)clear; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCChannelPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannel.h" #import "IRCChannelMemberListPrivate.h" #import "TVCLogController.h" NS_ASSUME_NONNULL_BEGIN @class TVCLogLine; @interface IRCChannel () @property (nonatomic, assign, readwrite) IRCChannelStatus status; @property (nonatomic, assign) BOOL sentInitialWhoRequest; @property (nonatomic, assign) BOOL channelModesReceived; @property (nonatomic, assign) BOOL channelNamesReceived; @property (nonatomic, assign, readwrite) BOOL errorOnLastJoinAttempt; - (instancetype)initWithConfig:(IRCChannelConfig *)config NS_DESIGNATED_INITIALIZER; - (instancetype)initWithConfigDictionary:(NSDictionary *)dic; - (void)updateConfig:(IRCChannelConfig *)config; - (void)updateConfig:(IRCChannelConfig *)config fireChangedNotification:(BOOL)fireChangedNotification; - (void)updateConfig:(IRCChannelConfig *)config fireChangedNotification:(BOOL)fireChangedNotification updateStoredChannelList:(BOOL)updateStoredChannelList; - (NSDictionary *)configurationDictionary; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (void)noteEncryptionStateDidChange; #endif - (void)writeToLogLineToLogFile:(TVCLogLine *)logLine; - (void)logFileWriteSessionBegin; - (void)logFileWriteSessionEnd; - (void)print:(TVCLogLine *)logLine; - (void)print:(TVCLogLine *)logLine completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock; - (void)reopenLogFileIfNeeded; - (void)closeLogFile; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCChannelUserPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelUser.h" NS_ASSUME_NONNULL_BEGIN @interface IRCChannelUser () - (instancetype)initWithUser:(IRCUser *)user; - (void)changeUserToUser:(IRCUser *)user; - (void)conversation; - (void)incomingConversation; - (void)outgoingConversation; + (NSComparator)channelRankComparator; + (NSComparator)nicknameLengthComparator; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCClientConfigPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClientConfig.h" NS_ASSUME_NONNULL_BEGIN @interface IRCClientConfig () /* To provide user with similar behavior, when migrating -connectionPrefersIPv4 in IRCClientConfig, we set the address type to IRCConnectionAddressTypeIPv4. We check if both values are set to offer the user a warning that the preference they had has changed in a way they may not want. When user changes the address type in Server Properties, we unset -connectionPrefersIPv4 so that the warning does not appear again, ever. */ @property (readonly) BOOL showConnectionPrefersIPv4Warning; @property (readonly) BOOL connectionPrefersIPv4 TEXTUAL_DEPRECATED("Use -addressType instead"); - (void)writeNicknamePasswordToKeychain; - (void)writeProxyPasswordToKeychain; - (void)destroyNicknamePasswordKeychainItem; - (void)destroyProxyPasswordKeychainItem; - (void)destroyServerPasswordKeychainItemAfterMigration; @end @interface IRCClientConfigMutable () @property (nonatomic, assign, readwrite) BOOL connectionPrefersIPv4 TEXTUAL_DEPRECATED("Use -addressType instead"); @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCClientPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLONotificationController.h" #import "IRCClient.h" NS_ASSUME_NONNULL_BEGIN @class TLOSpokenNotification; @class IRCAddressBookUserTrackingContainer, IRCTimedCommand, IRCUserMutable; enum { ClientIRCv3SupportedCapabilitySASLGeneric = 1 << 22, ClientIRCv3SupportedCapabilitySASLPlainText = 1 << 23, // YES if SASL=plain CAP is supported ClientIRCv3SupportedCapabilitySASLExternal = 1 << 24, // YES if SASL=external CAP is supported ClientIRCv3SupportedCapabilityZNCServerTime = 1 << 25, // YES if the ZNC vendor specific CAP supported ClientIRCv3SupportedCapabilityZNCServerTimeISO = 1 << 26, // YES if the ZNC vendor specific CAP supported ClientIRCv3SupportedCapabilityZNCPlaybackModule = 1 << 27, // YES if the ZNC vendor specific CAP supported ClientIRCv3SupportedCapabilityPlanioPlayback = 1 << 28 // YES if the plan.io vendor specific CAP supported. }; @interface IRCClient () @property (nonatomic, copy, nullable) dispatch_block_t disconnectCallback; @property (nonatomic, assign, readwrite) IRCClientConnectMode connectType; @property (nonatomic, assign, readwrite) IRCClientDisconnectMode disconnectType; @property (nonatomic, assign) BOOL sidebarItemIsExpanded; @property (nonatomic, copy, readwrite) NSArray *channelList; @property (nonatomic, weak, readwrite) IRCChannel *lastSelectedChannel; - (instancetype)initWithConfig:(IRCClientConfig *)config NS_DESIGNATED_INITIALIZER; - (instancetype)initWithConfigDictionary:(NSDictionary *)dic; - (void)updateConfig:(IRCClientConfig *)config; - (void)updateConfig:(IRCClientConfig *)config updateSelection:(BOOL)updateSelection; - (NSDictionary *)configurationDictionary; - (void)addChannel:(IRCChannel *)channel; - (void)addChannel:(IRCChannel *)channel atPosition:(NSUInteger)position; - (void)removeChannel:(IRCChannel *)channel; // This only removes the channel from channel array. Use world controller to properly destroy a channel. - (NSUInteger)indexOfChannel:(IRCChannel *)channel; - (void)selectFirstChannelInChannelList; - (void)reloadServerListItems; - (void)updateStoredChannelList; - (void)cacheHighlightInChannel:(IRCChannel *)channel withLogLine:(TVCLogLine *)logLine; - (void)inputText:(id)string destination:(IRCTreeItem *)destination; - (void)inputText:(id)string asCommand:(IRCRemoteCommand)command; - (void)inputText:(id)string asCommand:(IRCRemoteCommand)command destination:(IRCTreeItem *)destination; - (void)enableCapability:(ClientIRCv3SupportedCapability)capability; - (void)disableCapability:(ClientIRCv3SupportedCapability)capability; - (void)noteReachabilityChanged:(BOOL)reachable; - (void)autoConnectWithDelay:(NSUInteger)delay afterWakeUp:(BOOL)afterWakeUp; - (void)postEventToViewController:(NSString *)eventToken; - (void)postEventToViewController:(NSString *)eventToken forChannel:(IRCChannel *)channel; - (void)sendFile:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken; - (void)sendFileResume:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken; - (void)sendFileResumeAccept:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken; - (void)notifyFileTransfer:(TXNotificationType)type nickname:(NSString *)nickname filename:(NSString *)filename filesize:(uint64_t)totalFilesize requestIdentifier:(NSString *)identifier; - (IRCAddressBookUserTrackingContainer *)trackedUsers; - (IRCUserMutable *)mutableCopyOfUserWithNickname:(NSString *)nickname; - (void)modifyUser:(IRCUser *)user withBlock:(void (NS_NOESCAPE ^)(IRCUserMutable *userMutable))block; - (void)modifyUserUserWithNickname:(NSString *)nickname withBlock:(void (NS_NOESCAPE ^)(IRCUserMutable *userMutable))block; - (void)reopenLogFileIfNeeded; - (void)closeLogFile; - (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName isUtility:(BOOL)isUtility; - (nullable NSString *)formatNotificationToSpeak:(TLOSpokenNotification *)notification; - (id)queuedBatchMessageWithToken:(NSString *)batchToken; - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command escapeMessage:(BOOL)escapeMessage; - (void)onTimedCommand:(IRCTimedCommand *)timedCommand; - (void)logFileRecordSessionChanged:(BOOL)toNewSession inChannel:(nullable IRCChannel *)channel; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCClientRequestedCommandsPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCCommandIndex.h" NS_ASSUME_NONNULL_BEGIN /* The response of some commands are shown and hidden depending on who sent the command. IRCClientRequestedCommands is used by IRCClient internally to keep track of what commands have been requested so that it can determine how to treat the response. */ /* IRCClientRequestedCommands works in conjunction with IRCClient by balancing all calls to a command. */ @interface IRCClientRequestedCommands : NSObject - (void)removeCommands; @end #pragma mark - @interface IRCClientRequestedCommands (Helpers) #pragma mark - #pragma mark ISON Command (Default: hidden) @property (readonly, getter=inVisibleIsonRequest) BOOL visibleIsonRequest; - (void)recordIsonRequestOpened; - (void)recordIsonRequestOpenedAsVisible; - (void)recordIsonRequestClosed; #pragma mark - #pragma mark MONITOR Command (Default: hidden) #if 0 /* The MONITOR command can perform multiple actions which means its results do not always have a predetermined number of results or an end numeric. */ /* To work around this, we feed IRCClientRequestedCommands an estimate of the number of responses to expect. We then decrement that by calling the *ClosedOne for each response received. When the count reaches zero, the request is automatically closed without calling *Closed. */ /* If we encounter an error, we instead call *Closed which ends the request there. */ @property (readonly, getter=inVisibleMonitorRequest) BOOL visibleMonitorRequest; - (void)recordMonitorRequestOpened; // No limit on count - (void)recordMonitorRequestOpenedWithCount:(NSUInteger)count; - (void)recordMonitorRequestOpenedAsVisible; // No limit on count - (void)recordMonitorRequestOpenedAsVisibleWithCount:(NSUInteger)count; - (void)recordMonitorRequestClosedOne; // Does nothing if no count is specified - (void)recordMonitorRequestClosed; #pragma mark - #pragma mark NAMES Command (Default: hidden) @property (readonly, getter=inVisibleNamesRequest) BOOL visibleNamesRequest; - (void)recordNamesRequestOpened; - (void)recordNamesRequestOpenedAsVisible; - (void)recordNamesRequestClosed; #pragma mark - #pragma mark WATCH Command (Default: hidden) @property (readonly, getter=inVisibleWatchRequest) BOOL visibleWatchRequest; - (void)recordWatchRequestOpened; // No limit on count - (void)recordWatchRequestOpenedWithCount:(NSUInteger)count; - (void)recordWatchRequestOpenedAsVisible; // No limit on count - (void)recordWatchRequestOpenedAsVisibleWithCount:(NSUInteger)count; - (void)recordWatchRequestClosedOne; // Does nothing if no count is specified - (void)recordWatchRequestClosed; #endif #pragma mark - #pragma mark WHO Command (Default: hidden) @property (readonly, getter=inVisibleWhoRequest) BOOL visibleWhoRequest; - (void)recordWhoRequestOpened; - (void)recordWhoRequestOpenedAsVisible; - (void)recordWhoRequestClosed; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCColorFormatPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCLogLine.h" #import "IRCColorFormat.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel; @interface NSAttributedString (IRCTextFormatterPrivate) /* Given contextual information (client, channel, lineType), the original attributed string is converted to use appropriate formatting characters, but this method does not allow the result to exceed TXMaximumIRCBodyLength. */ /* The only valid lineType value is PRIVMSG, ACTION, or NOTICE. */ /* effectiveRange is the range of the result in the attributed string. Use this value to delete those characters during enumeration. */ - (NSString *)stringFormattedForChannel:(NSString *)channelName onClient:(IRCClient *)client withLineType:(TVCLogLineType)lineType effectiveRange:(NSRange * _Nullable)effectiveRange; @end @interface NSMutableAttributedString (IRCTextFormatterPrivate) /* This method truncates self to the effective range */ - (NSString *)stringFormattedForChannel:(NSString *)channelName onClient:(IRCClient *)client withLineType:(TVCLogLineType)lineType; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCCommandIndexPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCCommandIndex.h" NS_ASSUME_NONNULL_BEGIN @interface IRCCommandIndex () + (void)populateCommandIndex; + (void)invalidateCaches; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCConnectionPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCConnection.h" NS_ASSUME_NONNULL_BEGIN @interface IRCConnection () @property (nonatomic, copy, readwrite) IRCConnectionConfig *config; @property (nonatomic, assign, readwrite) BOOL isConnected; @property (nonatomic, assign, readwrite) BOOL isConnecting; @property (nonatomic, assign, readwrite) BOOL isDisconnecting; @property (nonatomic, assign, readwrite) BOOL isSending; @property (nonatomic, assign, readwrite) BOOL isSecured; @property (nonatomic, assign, readwrite) BOOL isConnectedWithClientSideCertificate; @property (nonatomic, assign, readwrite) BOOL EOFReceived; @property (nonatomic, copy, readwrite, nullable) NSString *connectedAddress; - (void)enforceFloodControl; - (void)openSecuredConnectionCertificateModal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCExtrasPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface IRCExtras : NSObject + (void)parseIRCProtocolURI:(NSString *)location; + (void)parseIRCProtocolURI:(NSString *)location withDescriptor:(nullable NSAppleEventDescriptor *)event; + (void)createConnectionToServer:(NSString *)serverInfo channelList:(nullable NSString *)channelList connectWhenCreated:(BOOL)connectWhenCreated; + (void)createConnectionToServer:(NSString *)serverInfo channelList:(nullable NSString *)channelList connectWhenCreated:(BOOL)connectWhenCreated mergeConnectionIfPossible:(BOOL)mergeConnectionIfPossible selectFirstChannelAdded:(BOOL)selectFirstChannelAdded; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCHighlightLogEntryPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCHighlightLogEntry.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel; @interface IRCHighlightLogEntry () @property (readonly, weak) IRCChannel *channel; @property (readonly, copy) NSString *channelName; @property (readonly, copy) NSString *timeLoggedFormatted; @end @interface IRCHighlightLogEntryMutable : IRCHighlightLogEntry @property (nonatomic, copy, readwrite) TVCLogLine *lineLogged; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, copy, readwrite) NSString *channelId; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCISupportInfoPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCISupportInfo.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient; @interface IRCISupportInfo () @property (nonatomic, copy, readwrite, nullable) NSString *serverAddress; @property (readonly, copy, nullable) NSString *stringValueForLastUpdate; - (instancetype)initWithClient:(IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)processConfigurationData:(NSString *)configurationData; - (NSArray *)parseModes:(NSString *)modeString; - (void)reset; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCMessageBatchPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN /* Each IRCClient is assigned a single instance of IRCMessageBatchMessageContainer which acts as a container for all BATCH command events that the client may receive. */ @interface IRCMessageBatchMessageContainer : NSObject @property (readonly, copy) NSDictionary *queuedEntries; - (void)queueEntry:(id)entry; - (void)dequeueEntry:(id)entry; - (void)dequeueEntries; - (id)queuedEntryWithBatchToken:(NSString *)batchToken; @end /* IRCMessageBatchMessage represents a single BATCH event based on its token value. Queued entries can either be an IRCMessage instance or IRCMessageBatchMessage (for nested batch events). */ @interface IRCMessageBatchMessage : NSObject @property (nonatomic, assign) BOOL batchIsOpen; @property (nonatomic, copy) NSString *batchToken; @property (nonatomic, copy, nullable) NSString *batchType; @property (readonly, copy) NSArray *queuedEntries; @property (nonatomic, weak) IRCMessageBatchMessage *parentBatchMessage; - (void)queueEntry:(id)entry; - (void)dequeueEntry:(id)entry; - (void)dequeueEntries; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCMessagePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCMessage.h" NS_ASSUME_NONNULL_BEGIN @class IRCMessageBatchMessage; @interface IRCMessage () @property (readonly, strong, nullable) IRCMessageBatchMessage *parentBatchMessage; - (void)markAsNotHistoric; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCServerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCServer.h" NS_ASSUME_NONNULL_BEGIN @interface IRCServer () /* IRCClient retains a copy of the active IRCServer instance. When the -serverList in IRCClientConfig is modified, any servers that are removed will have their keychain items destroyed. We do not want to destroy the keychain items for the IRCServer instance that IRCClient has a copy of, so the -destroyKeychainItemsDuringDealloc flag is used. This tells the IRCServer instance to hold onto the keychain item until there is no longer a reference to it. */ @property (nonatomic, assign) BOOL destroyKeychainItemsDuringDealloc; - (void)writeServerPasswordToKeychain; - (void)destroyServerPasswordKeychainItem; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCTimerCommandPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel; @interface IRCTimedCommand : NSObject @property (readonly, copy) NSString *identifier; @property (readonly, copy) NSString *clientId; @property (readonly, copy, nullable) NSString *channelId; @property (readonly, copy) NSString *command; @property (readonly) NSTimeInterval startTime; @property (readonly) NSTimeInterval timeRemaining; @property (readonly) NSTimeInterval timerInterval; @property (readonly) BOOL timerIsActive; @property (readonly) BOOL repeatTimer; @property (readonly) NSUInteger iterations; @property (readonly) NSUInteger currentIteration; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithCommand:(NSString *)command onClient:(IRCClient *)client; - (instancetype)initWithCommand:(NSString *)command onClient:(IRCClient *)client inChannel:(nullable IRCChannel *)channel NS_DESIGNATED_INITIALIZER; - (void)start:(NSTimeInterval)interval; // repeatTimer = NO - (void)start:(NSTimeInterval)timerInterval onRepeat:(BOOL)repeatTimer; // iterations = 0 - (void)start:(NSTimeInterval)timerInterval onRepeat:(BOOL)repeatTimer iterations:(NSUInteger)iterations; // 0 iterations = infinite - (void)stop; /* If the timer has been started before, then we already know the values to pass to -start: In that case, we return YES. If we don't know this information yet, then we return NO. */ /* Restarting the timer resets the iteration count. */ - (BOOL)restart; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCTreeItemPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCTreeItem.h" NS_ASSUME_NONNULL_BEGIN @interface IRCTreeItem () @property (nonatomic, assign, readwrite) NSUInteger dockUnreadCount; @property (nonatomic, assign, readwrite) NSUInteger nicknameHighlightCount; @property (nonatomic, assign, readwrite) NSUInteger treeUnreadCount; @property (nonatomic, weak, readwrite) IRCClient *associatedClient; @property (nonatomic, strong, readwrite) TVCLogController *viewController; - (void)resetState; @property (readonly) NSUInteger numberOfChildren; - (nullable IRCTreeItem *)childAtIndex:(NSUInteger)index; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCUserNicknameColorStyleGeneratorPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCTheme.h" NS_ASSUME_NONNULL_BEGIN @interface IRCUserNicknameColorStyleGenerator : NSObject + (NSString *)nicknameColorStyleForString:(NSString *)inputString; + (NSString *)nicknameColorStyleForString:(NSString *)inputString isOverride:(BOOL * _Nullable)isOverride; + (NSNumber *)hashForString:(NSString *)inputString colorStyle:(TPCThemeSettingsNicknameColorStyle)colorStyle; + (nullable NSColor *)nicknameColorStyleOverrideForKey:(NSString *)styleKey; + (void)setNicknameColorStyleOverride:(nullable NSColor *)styleValue forKey:(NSString *)styleKey; + (void)migrateNicknameColorStyleOverrides; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCUserPersistentStorePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN /* IRCUserPersistentStore is a class used by IRCUser to store properties which are persistent between multiple read-only and mutable copies of the same IRCUser instance. */ @class IRCUserRelations; @interface IRCUserPersistentStore : NSObject @property (nonatomic, strong) IRCUserRelations *relations; @property (nonatomic, assign) CFAbsoluteTime presentAwayMessageFor301LastEvent; @property (nonatomic, strong) dispatch_source_t removeUserTimer; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCUserPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCUser.h" NS_ASSUME_NONNULL_BEGIN @interface IRCUser () - (IRCClient *)client; - (void)cancelRemoveUserTimer; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCUserRelationsPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelUser.h" #import "IRCUserRelations.h" NS_ASSUME_NONNULL_BEGIN /* IRCUserRelations is a class used by IRCUser to store which IRCChannelUser object is associated with a particular channel. */ @interface IRCUserRelations : NSObject @property (readonly, copy) NSArray *relatedChannels; @property (readonly, copy) NSArray *relatedUsers; @property (readonly, copy) NSDictionary *relations; @property (readonly) NSUInteger numberOfRelations; - (void)associateUser:(IRCChannelUser *)user withChannel:(IRCChannel *)channel; - (void)disassociateUserWithChannel:(IRCChannel *)channel; - (nullable IRCChannelUser *)userAssociatedWithChannel:(IRCChannel *)channel; - (void)enumerateRelations:(void (NS_NOESCAPE ^)(IRCChannel *channel, IRCChannelUser *member, BOOL *stop))block; @end #pragma mark - /* Acts as easy access for internal relations object */ @interface IRCUser (IRCUserRelationsPrivate) - (void)becamePrimaryUser; - (void)associateUser:(IRCChannelUser *)user withChannel:(IRCChannel *)channel; - (void)disassociateUserWithChannel:(IRCChannel *)channel; - (nullable IRCChannelUser *)userAssociatedWithChannel:(IRCChannel *)channel; - (void)enumerateRelations:(void (NS_NOESCAPE ^)(IRCChannel *channel, IRCChannelUser *member, BOOL *stop))block; @end @interface IRCChannelUser (IRCUserRelationsPrivate) - (void)associateWithChannel:(IRCChannel *)channel; - (void)disassociateWithChannel:(IRCChannel *)channel; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/IRCWorldPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelConfigPrivate.h" #import "IRCWorld.h" NS_ASSUME_NONNULL_BEGIN @interface IRCWorld () @property (nonatomic, assign, readwrite) NSUInteger messagesSent; @property (nonatomic, assign, readwrite) NSUInteger messagesReceived; @property (nonatomic, assign, readwrite) uint64_t bandwidthIn; @property (nonatomic, assign, readwrite) uint64_t bandwidthOut; @property (nonatomic, assign) BOOL isImportingConfiguration; @property (nonatomic, copy, readwrite) NSArray *clientList; - (void)setupConfiguration; - (nullable IRCTreeItem *)findItemWithPasteboardString:(NSString *)string; - (NSString *)pasteboardStringForItem:(IRCTreeItem *)item; - (void)autoConnectAfterWakeup:(BOOL)afterWakeUp; - (void)prepareForSleep; - (void)prepareForScreenSleep; - (void)wakeFromScreenSleep; - (void)noteReachabilityChanged:(BOOL)reachable; - (IRCClient *)createClientWithConfig:(IRCClientConfig *)config reload:(BOOL)reload; - (IRCChannel *)createChannelWithConfig:(IRCChannelConfig *)config onClient:(IRCClient *)client add:(BOOL)add adjust:(BOOL)adjust reload:(BOOL)reload; - (IRCChannel *)createPrivateMessage:(NSString *)nickname onClient:(IRCClient *)client asType:(IRCChannelType)type; - (void)destroyClient:(IRCClient *)client; - (void)destroyChannel:(IRCChannel *)channel reload:(BOOL)reload; - (void)destroyChannel:(IRCChannel *)channel reload:(BOOL)reload part:(BOOL)partChannel; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/NSTableVIewHelperPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface NSTableView (TXTableViewHelper) + (NSFont *)preferredGlobalTableViewFont; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/NSViewHelperPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCMainWindow; @interface NSView (TXViewHelperPrivate) /* If this view is attached to an instance of TXMainWindow, then this returns the instance its attached to. */ @property (readonly, nullable) TVCMainWindow *mainWindow; @end @interface NSCell (TXCellHelperPrivate) @property (readonly, nullable) NSWindow *window; @property (readonly, nullable) TVCMainWindow *mainWindow; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/SwiftBridgingHeaderPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TextualPrivate.h" // TLOpenLink.swift #import "TPCPreferencesLocal.h" // TLOLinkParser.swift #import "TVCLogLine.h" ================================================ FILE: Sources/App/Classes/Headers/Private/TDCAboutDialogPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCWindowBase.h" NS_ASSUME_NONNULL_BEGIN @protocol TDCAboutDialogDelegate; @interface TDCAboutDialog : TDCWindowBase @end @protocol TDCAboutDialogDelegate @required - (void)aboutDialogWillClose:(TDCAboutDialog *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCAddressBookSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCAddressBook.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @protocol TDCAddressBookSheetDelegate; @interface TDCAddressBookSheet : TDCSheetBase - (instancetype)initWithEntryType:(IRCAddressBookEntryType)entryType NS_DESIGNATED_INITIALIZER; - (instancetype)initWithConfig:(IRCAddressBookEntry *)config NS_DESIGNATED_INITIALIZER; - (void)start; @end @protocol TDCAddressBookSheetDelegate @required - (void)addressBookSheet:(TDCAddressBookSheet *)sender onOk:(IRCAddressBookEntry *)config; - (void)addressBookSheetWillClose:(TDCAddressBookSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelBanListSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCISupportInfo.h" #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel; typedef NS_ENUM(NSUInteger, TDCChannelBanListSheetEntryType) { TDCChannelBanListSheetEntryTypeBan = IRCISupportInfoListTypeBan, TDCChannelBanListSheetEntryTypeBanException = IRCISupportInfoListTypeBanException, TDCChannelBanListSheetEntryTypeInviteException = IRCISupportInfoListTypeInviteException, TDCChannelBanListSheetEntryTypeQuiet = IRCISupportInfoListTypeQuiet }; @interface TDCChannelBanListSheet : TDCSheetBase @property (readonly) TDCChannelBanListSheetEntryType entryType; @property (readonly, copy) NSString *modeSymbol; @property (readonly, copy, nullable) NSArray *listOfChanges; @property (nonatomic, assign) BOOL contentAlreadyReceived; /* Returns nil if entry type is not supported by client */ - (nullable instancetype)initWithEntryType:(TDCChannelBanListSheetEntryType)entryType inChannel:(IRCChannel *)channel NS_DESIGNATED_INITIALIZER; - (void)start; - (void)clear; - (void)addEntry:(NSString *)entryMask setBy:(nullable NSString *)entryAuthor creationDate:(nullable NSDate *)entryCreationDate; @end @protocol TDCChannelBanListSheetDelegate @required - (void)channelBanListSheetOnUpdate:(TDCChannelBanListSheet *)sender; - (void)channelBanListSheetWillClose:(TDCChannelBanListSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelInviteSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient; @interface TDCChannelInviteSheet : TDCSheetBase @property (readonly, copy) NSArray *nicknames; - (instancetype)initWithNicknames:(NSArray *)nicknames onClient:(IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)startWithChannels:(NSArray *)channels; @end @protocol TDCChannelInviteSheetDelegate @required - (void)channelInviteSheet:(TDCChannelInviteSheet *)sender onSelectChannel:(NSString *)channelName; - (void)channelInviteSheetWillClose:(TDCChannelInviteSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelModifyModesSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel, IRCChannelModeContainer; @interface TDCChannelModifyModesSheet : TDCSheetBase - (instancetype)initWithChannel:(IRCChannel *)channel NS_DESIGNATED_INITIALIZER; - (void)start; @end @protocol TDCChannelModifyModesSheetDelegate @required - (void)channelModifyModesSheet:(TDCChannelModifyModesSheet *)sender onOk:(IRCChannelModeContainer *)modes; - (void)channelModifyModesSheetWillClose:(TDCChannelModifyModesSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelModifyTopicSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel; @interface TDCChannelModifyTopicSheet : TDCSheetBase - (instancetype)initWithChannel:(IRCChannel *)channel NS_DESIGNATED_INITIALIZER; - (void)start; @end @protocol TDCChannelModifyTopicSheetDelegate @required - (void)channelModifyTopicSheet:(TDCChannelModifyTopicSheet *)sender onOk:(NSString *)topic; - (void)channelModifyTopicSheetWillClose:(TDCChannelModifyTopicSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelPropertiesNotificationConfigurationPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCChannelPropertiesSheetPrivate.h" #import "TLONotificationController.h" #import "TLONotificationConfigurationPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelPropertiesNotificationConfiguration : TLONotificationConfiguration - (instancetype)initWithEventType:(TXNotificationType)aEventType inSheet:(TDCChannelPropertiesSheet *)sheet; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelPropertiesSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel, IRCChannelConfig; @protocol TDCChannelPropertiesSheetDelegate; @interface TDCChannelPropertiesSheet : TDCSheetBase - (instancetype)initWithChannel:(IRCChannel *)channel NS_DESIGNATED_INITIALIZER; - (instancetype)initWithClient:(IRCClient *)client; - (instancetype)initWithClientId:(NSString *)clientId; - (instancetype)initWithConfig:(nullable IRCChannelConfig *)config; - (instancetype)initWithConfig:(nullable IRCChannelConfig *)config onClientWithId:(nullable NSString *)clientId; - (instancetype)initWithConfig:(nullable IRCChannelConfig *)config onClient:(nullable IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)start; @end @protocol TDCChannelPropertiesSheetDelegate @required - (void)channelPropertiesSheet:(TDCChannelPropertiesSheet *)sender onOk:(IRCChannelConfig *)config; - (void)channelPropertiesSheetWillClose:(TDCChannelPropertiesSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelSpotlightAppearancePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCAppearance.h" NS_ASSUME_NONNULL_BEGIN @interface TDCChannelSpotlightAppearance : TVCApplicationAppearance #pragma mark - #pragma mark Search Field @property (readonly, copy, nullable) NSColor *searchFieldTextColor; @property (readonly, copy, nullable) NSColor *searchFieldCompletionTextColor; @property (readonly, copy, nullable) NSColor *searchFieldNoResultsTextColor; #pragma mark - #pragma mark Search Result @property (readonly, copy, nullable) NSColor *searchResultRowSelectionColorActiveWindow; @property (readonly, copy, nullable) NSColor *searchResultRowSelectionColorInactiveWindow; @property (readonly) BOOL searchResultRowEmphasized; @property (readonly, copy, nullable) NSColor *searchResultChannelNameTextColor; @property (readonly, copy, nullable) NSColor *searchResultChannelDescriptionTextColor; @property (readonly, copy, nullable) NSColor *searchResultKeyboardShortcutTextColor; @property (readonly) CGFloat searchResultKeyboardShortcutDeselectedOffset; @property (readonly) CGFloat searchResultKeyboardShortcutSelectedOffset; @property (readonly, copy, nullable) NSColor *searchResultSelectedTextColor; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelSpotlightControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCWindowBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannel; @protocol TDCChannelSpotlightControllerDelegate; @interface TDCChannelSpotlightController : TDCWindowBase @end @protocol TDCChannelSpotlightControllerDelegate @required - (void)channelSpotlightController:(TDCChannelSpotlightController *)sender selectChannel:(IRCChannel *)channel; - (void)channelSpotlightControllerWillClose:(TDCChannelSpotlightController *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelSpotlightControlsPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TDCChannelSpotlightPanel : NSPanel @end @interface TDCChannelSpotlightTextField : NSTextField @end @interface TDCChannelSpotlightImageView : NSImageView @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelSpotlightSearchResultPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCChannel, TDCChannelSpotlightController; @interface TDCChannelSpotlightSearchResult : NSObject @property (readonly, weak) IRCChannel *channel; @property (readonly, copy) NSString *clientId; @property (readonly, copy) NSNumber *distance; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithChannel:(IRCChannel *)channel NS_DESIGNATED_INITIALIZER; - (void)recalculateDistanceWith:(NSString *)searchString; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCChannelSpotlightSearchResultsTablePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TDCChannelSpotlightController; @interface TDCChannelSpotlightSearchResultRowView : NSTableRowView - (instancetype)initWithController:(TDCChannelSpotlightController *)controller; @end @interface TDCChannelSpotlightSearchResultCellView : NSTableCellView @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCFileTransferDialogPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCWindowBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, TDCFileTransferDialogTransferController, TVCBasicTableView; typedef NS_ENUM(NSUInteger, TDCFileTransferDialogTransferStatus) { TDCFileTransferDialogTransferStatusComplete, TDCFileTransferDialogTransferStatusConnecting, TDCFileTransferDialogTransferStatusFatalError, TDCFileTransferDialogTransferStatusInitializing, TDCFileTransferDialogTransferStatusIsListeningAsReceiver, TDCFileTransferDialogTransferStatusIsListeningAsSender, TDCFileTransferDialogTransferStatusMappingListeningPort, TDCFileTransferDialogTransferStatusReceiving, TDCFileTransferDialogTransferStatusRecoverableError, TDCFileTransferDialogTransferStatusSending, TDCFileTransferDialogTransferStatusStopped, TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress, TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept, TDCFileTransferDialogTransferStatusWaitingForResumeAccept }; typedef NS_ENUM(NSUInteger, TDCFileTransferDialogSelection) { TDCFileTransferDialogSelectionAll = 0, TDCFileTransferDialogSelectionSending = 1, TDCFileTransferDialogSelectionReceiving = 2 }; @class TDCFileTransferDialogTransferController; @interface TDCFileTransferDialog : TDCWindowBase @property (readonly, weak) TVCBasicTableView *fileTransferTable; @property (readonly, copy, nullable) NSString *IPAddress; - (void)show:(BOOL)makeKeyWindow; - (void)show:(BOOL)makeKeyWindow restorePosition:(BOOL)restorePosition; - (void)requestIPAddress; // from external source - (void)clearIPAddress; /* The next two method return a unique identifier specific to each added request. This identifier is different from a request token as it is available always and never is seen by the other user. It is used internally for finding specific requests. */ /* Returning nil means that something failed and the transfer was never added to list of transfers. */ - (nullable NSString *)addReceiverForClient:(IRCClient *)client nickname:(NSString *)nickname address:(NSString *)hostAddress port:(uint16_t)hostPort filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken; - (nullable NSString *)addSenderForClient:(IRCClient *)client nickname:(NSString *)nickname path:(NSString *)path autoOpen:(BOOL)autoOpen; - (BOOL)fileTransferExistsWithToken:(NSString *)transferToken; - (nullable TDCFileTransferDialogTransferController *)fileTransferMatchingPort:(uint16_t)port; - (nullable TDCFileTransferDialogTransferController *)fileTransferSenderMatchingToken:(NSString *)transferToken; - (nullable TDCFileTransferDialogTransferController *)fileTransferReceiverMatchingToken:(NSString *)transferToken; - (nullable TDCFileTransferDialogTransferController *)fileTransferWithUniqueIdentifier:(NSString *)identifier; @end #pragma mark - @interface TDCFileTransferDialog (TDCFileTransferDialogDownloadDestinationExtension) - (nullable NSURL *)downloadDestinationURL; - (void)setDownloadDestinationURL:(nullable NSData *)downloadDestinationURL; - (void)startUsingDownloadDestinationURL; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCFileTransferDialogTableCellPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TDCFileTransferDialogTableCell : NSTableCellView - (void)onMaintenanceTimer; - (void)reloadStatusInformation; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCFileTransferDialogTransferControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCFileTransferDialogPrivate.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, TDCFileTransferDialogTableCell; @interface TDCFileTransferDialogTransferController : NSObject @property (nonatomic, weak) TDCFileTransferDialogTableCell *transferTableCell; @property (readonly) BOOL isResume; @property (readonly) BOOL isReversed; @property (readonly) BOOL isSender; @property (readonly) TDCFileTransferDialogTransferStatus transferStatus; @property (readonly) uint64_t totalFilesize; @property (readonly) uint64_t processedFilesize; @property (readonly) uint64_t currentRecord; @property (readonly, copy) NSArray *speedRecords; @property (readonly, copy, nullable) NSString *errorMessageDescription; @property (readonly, copy, nullable) NSString *path; @property (readonly, copy) NSString *filename; @property (readonly, copy, nullable) NSString *filePath; @property (readonly, copy, nullable) NSURL *fileURL; @property (readonly, copy) NSString *hostAddress; @property (readonly, copy) NSString *peerNickname; @property (readonly, copy, nullable) NSString *transferToken; @property (readonly, copy) NSString *uniqueIdentifier; @property (readonly) uint16_t hostPort; @property (getter=isActingAsClient, readonly) BOOL actingAsClient; @property (getter=isActingAsServer, readonly) BOOL actingAsServer; + (nullable instancetype)receiverForClient:(IRCClient *)client nickname:(NSString *)nickname address:(NSString *)hostAddress port:(uint16_t)hostPort filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken; + (nullable instancetype)senderForClient:(IRCClient *)client nickname:(NSString *)nickname path:(NSString *)path; - (instancetype)init NS_UNAVAILABLE; - (void)open; - (void)openWithPathOrUserDownloads; - (void)openWithPath:(nullable NSString *)path; // Only changes path if self.path == nil - (void)close; - (void)closeAndPostNotification:(BOOL)postNotification; - (void)sendTransferRequestToClient; - (void)noteIPAddressLookupFailed; - (void)noteIPAddressLookupSucceeded; - (void)didReceiveSendRequest:(NSString *)hostAddress hostPort:(uint16_t)hostPort; - (void)didReceiveResumeAccept:(uint64_t)proposedPosition; - (void)didReceiveResumeRequest:(uint64_t)proposedPosition; - (void)onMaintenanceTimer; - (void)updateClearButton; - (void)reloadStatusInformation; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCHighlightEntrySheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCChannelConfig, IRCHighlightMatchCondition; @interface TDCHighlightEntrySheet : TDCSheetBase - (instancetype)initWithConfig:(nullable IRCHighlightMatchCondition *)config NS_DESIGNATED_INITIALIZER; - (void)startWithChannels:(NSArray *)channels; @end @protocol TDCHighlightEntrySheetDelegate @required - (void)highlightEntrySheet:(TDCHighlightEntrySheet *)sender onOk:(IRCHighlightMatchCondition *)config; - (void)highlightEntrySheetWillClose:(TDCHighlightEntrySheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCLicenseManagerDialogPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCWindowBase.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 TEXTUAL_EXTERN NSNotificationName const TDCLicenseManagerActivatedLicenseNotification; TEXTUAL_EXTERN NSNotificationName const TDCLicenseManagerDeactivatedLicenseNotification; TEXTUAL_EXTERN NSNotificationName const TDCLicenseManagerTrialExpiredNotification; @interface TDCLicenseManagerDialog : TDCWindowBase - (void)activateLicenseKey:(NSString *)licenseKey; - (void)activateLicenseKey:(NSString *)licenseKey silently:(BOOL)silently; - (NSString *)timeRemainingInTrialFormattedMessage; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCLicenseManagerMigrateAppStoreSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @interface TDCLicenseManagerMigrateAppStoreSheet : TDCSheetBase - (void)start; @end @protocol TDCLicenseManagerMigrateAppStoreSheetDelegate @required - (void)licenseManagerMigrateAppStoreSheet:(TDCLicenseManagerMigrateAppStoreSheet *)sender convertReceipt:(NSString *)receiptData licenseOwnerName:(NSString *)licenseOwnerName licenseOwnerContactAddress:(NSString *)licenseOwnerContactAddress; - (void)licenseManagerMigrateAppStoreSheetWillClose:(TDCLicenseManagerMigrateAppStoreSheet *)sender; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCLicenseManagerRecoverLostLicenseSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @interface TDCLicenseManagerRecoverLostLicenseSheet : TDCSheetBase - (void)start; @end @protocol TDCLicenseManagerRecoverLostLicenseSheet @required - (void)licenseManagerRecoverLostLicenseSheet:(TDCLicenseManagerRecoverLostLicenseSheet *)sender onOk:(NSString *)contactAddress; - (void)licenseManagerRecoverLostLicenseSheetWillClose:(TDCLicenseManagerRecoverLostLicenseSheet *)sender; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCLicenseUpgradeActivateSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" #import "TDCLicenseUpgradeEligibilitySheetPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @protocol TDCLicenseUpgradeActivateSheetDelegate; @interface TDCLicenseUpgradeActivateSheet : TDCSheetBase @property (readonly, copy) NSString *licenseKey; @property (readonly) TLOLicenseUpgradeEligibility eligibility; - (instancetype)initWithLicenseKey:(NSString *)licenseKey eligibility:(TLOLicenseUpgradeEligibility)eligibility NS_DESIGNATED_INITIALIZER; - (void)start; @end @protocol TDCLicenseUpgradeActivateSheetDelegate @required - (void)upgradeActivateSheetActivateLicense:(TDCLicenseUpgradeActivateSheet *)sender; - (void)upgradeActivateSheetPurchaseUpgrade:(TDCLicenseUpgradeActivateSheet *)sender; - (void)upgradeActivateSheetSuppressed:(TDCLicenseUpgradeActivateSheet *)sender; - (void)upgradeActivateSheetWillClose:(TDCLicenseUpgradeActivateSheet *)sender; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCLicenseUpgradeCommonActionsPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @interface TDCLicenseUpgradeCommonActions : NSObject + (void)contactSupport; + (void)activateLicense:(NSString *)licenseKey; + (void)purchaseUpgradeForLicense:(NSString *)licenseKey; + (void)learnMore; + (void)openStandaloneStore; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCLicenseUpgradeDialogPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCWindowBase.h" #import "TLOLicenseManagerLastGenPrivate.h" #import "TDCLicenseUpgradeEligibilitySheetPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @protocol TDCLicenseUpgradeDialogDelegate; @interface TDCLicenseUpgradeDialog : TDCWindowBase @property (readonly, copy) NSString *licenseKey; @property (readonly) TLOLicenseUpgradeEligibility eligibility; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithLicenseKey:(NSString *)licenseKey NS_DESIGNATED_INITIALIZER; @end @protocol TDCLicenseUpgradeDialogDelegate @required - (void)licenseUpgradeDialogEligibilityChanged:(TDCLicenseUpgradeDialog *)sender; - (void)licenseUpgradeDialogWRemindMeLater:(TDCLicenseUpgradeDialog *)sender; - (void)licenseUpgradeDialogWillClose:(TDCLicenseUpgradeDialog *)sender; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCLicenseUpgradeEligibilitySheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 typedef NS_ENUM(NSUInteger, TLOLicenseUpgradeEligibility) { TLOLicenseUpgradeEligibilityUnknown = LONG_MAX, TLOLicenseUpgradeEligibilityNot = 0, TLOLicenseUpgradeEligibilityDiscount = 1, TLOLicenseUpgradeEligibilityFree = 3, TLOLicenseUpgradeEligibilityAlreadyUpgraded = 2, }; @protocol TDCLicenseUpgradeEligibilitySheetDelegate; @interface TDCLicenseUpgradeEligibilitySheet : TDCSheetBase @property (readonly, copy) NSString *licenseKey; @property (readonly) TLOLicenseUpgradeEligibility eligibility; - (instancetype)initWithLicenseKey:(NSString *)licenseKey NS_DESIGNATED_INITIALIZER; - (void)checkEligibility; @end @protocol TDCLicenseUpgradeEligibilitySheetDelegate @required - (void)upgradeEligibilitySheetContactSupport:(TDCLicenseUpgradeEligibilitySheet *)sender; - (void)upgradeEligibilitySheetActivateLicense:(TDCLicenseUpgradeEligibilitySheet *)sender; - (void)upgradeEligibilitySheetPurchaseUpgrade:(TDCLicenseUpgradeEligibilitySheet *)sender; - (void)upgradeEligibilitySheetPurchaseStandalone:(TDCLicenseUpgradeEligibilitySheet *)sender; - (void)upgradeEligibilitySheetChanged:(TDCLicenseUpgradeEligibilitySheet *)sender; - (void)upgradeEligibilitySheetWillClose:(TDCLicenseUpgradeEligibilitySheet *)sender; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCNicknameColorSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @interface TDCNicknameColorSheet : TDCSheetBase - (instancetype)initWithNickname:(NSString *)nickname NS_DESIGNATED_INITIALIZER; - (void)start; @end @protocol TDCNicknameColorSheetDelegate @required - (void)nicknameColorSheetOnOk:(TDCNicknameColorSheet *)sender; - (void)nicknameColorSheetWillClose:(TDCNicknameColorSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCPreferencesControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCWindowBase.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TDCPreferencesControllerSelection) { TDCPreferencesControllerSelectionDefault = 0, TDCPreferencesControllerSelectionNotifications, TDCPreferencesControllerSelectionStyle, TDCPreferencesControllerSelectionHiddenPreferences }; @protocol TDCPreferencesControllerDelegate; @interface TDCPreferencesController : TDCWindowBase + (void)openProxySettingsInSystemPreferences; - (void)show:(TDCPreferencesControllerSelection)selection; @end @protocol TDCPreferencesControllerDelegate @required - (void)preferencesDialogWillClose:(TDCPreferencesController *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCPreferencesNotificationConfigurationPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLONotificationController.h" #import "TLONotificationConfigurationPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TDCPreferencesNotificationConfiguration : TLONotificationConfiguration + (TDCPreferencesNotificationConfiguration *)objectWithEventType:(TXNotificationType)eventType; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCPreferencesUserStyleSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @interface TDCPreferencesUserStyleSheet : TDCSheetBase - (void)start; @end @protocol TDCPreferencesUserStyleSheetDelegate @required - (void)userStyleSheetRulesChanged:(TDCPreferencesUserStyleSheet *)sender; - (void)userStyleSheetWillClose:(TDCPreferencesUserStyleSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCProgressIndicatorSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @interface TDCProgressIndicatorSheet : TDCSheetBase - (void)start; - (void)stop; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCServerChangeNicknameSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient; @interface TDCServerChangeNicknameSheet : TDCSheetBase - (instancetype)initWithClient:(IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)start; @end @protocol TDCServerChangeNicknameSheetDelegate @required - (void)serverChangeNicknameSheet:(TDCServerChangeNicknameSheet *)sender didInputNickname:(NSString *)nickname; - (void)serverChangeNicknameSheetWillClose:(TDCServerChangeNicknameSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCServerChannelListDialogPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCWindowBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient; @protocol TDCServerChannelListDialogDelegate; @interface TDCServerChannelListDialog : TDCWindowBase @property (nonatomic, assign) BOOL contentAlreadyReceived; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithClient:(IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)clear; - (void)addChannel:(NSString *)channel count:(NSUInteger)count topic:(nullable NSString *)topic; @end @protocol TDCServerChannelListDialogDelegate @required - (void)serverChannelListDialogOnUpdate:(TDCServerChannelListDialog *)sender; - (void)serverChannelListDialog:(TDCServerChannelListDialog *)sender joinChannels:(NSArray *)channels; - (void)serverChannelDialogWillClose:(TDCServerChannelListDialog *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCServerEndpointListSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCServer; @protocol TDCServerEndpointListSheetDelegate; @interface TDCServerEndpointListSheet : TDCSheetBase - (void)startWithServerList:(NSArray *)serverList; @end @protocol TDCServerEndpointListSheetDelegate @required - (void)serverEndpointListSheet:(TDCServerEndpointListSheet *)ender onOk:(NSArray *)serverList; - (void)serverEndpointListSheetWillClose:(TDCServerEndpointListSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCServerEndpointListSheetTablePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TDCServerEndpointListSheetTableCellView : NSTableCellView @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCServerHighlightListSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient; @interface TDCServerHighlightListSheet : TDCSheetBase - (instancetype)initWithClient:(IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)start; - (void)addEntry:(id)newEntry; @end @protocol TDCServerHighlightListSheetDelegate @required - (void)serverHighlightListSheetWillClose:(TDCServerHighlightListSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCServerPropertiesSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCClientConfig; typedef NS_ENUM(NSUInteger, TDCServerPropertiesSheetSelection) { TDCServerPropertiesSheetSelectionDefault = 0, TDCServerPropertiesSheetSelectionAddressBook = 1, TDCServerPropertiesSheetSelectionAutojoin = 2, TDCServerPropertiesSheetSelectionConnectCommands = 3, TDCServerPropertiesSheetSelectionEncoding = 4, TDCServerPropertiesSheetSelectionGeneral = 5, TDCServerPropertiesSheetSelectionIdentity = 6, TDCServerPropertiesSheetSelectionHighlights = 7, TDCServerPropertiesSheetSelectionDisconnectMessages = 8, TDCServerPropertiesSheetSelectionZncBouncer = 10, TDCServerPropertiesSheetSelectionClientCertificate = 12, TDCServerPropertiesSheetSelectionFloodControl = 13, TDCServerPropertiesSheetSelectionNetworkSocket = 14, TDCServerPropertiesSheetSelectionProxyServer = 15, TDCServerPropertiesSheetSelectionRedundancy = 16, TDCServerPropertiesSheetSelectionNewIgnoreEntry = 200 }; @protocol TDCServerPropertiesSheetDelegate; @interface TDCServerPropertiesSheet : TDCSheetBase - (instancetype)initWithClient:(nullable IRCClient *)client NS_DESIGNATED_INITIALIZER; - (void)startWithSelection:(TDCServerPropertiesSheetSelection)selection context:(nullable id)context; @end @protocol TDCServerPropertiesSheetDelegate @required - (void)serverPropertiesSheet:(TDCServerPropertiesSheet *)sender onOk:(IRCClientConfig *)config; - (void)serverPropertiesSheetWillClose:(TDCServerPropertiesSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCSharedProtocolDefinitionsPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel; @protocol TDCClientPrototype @required @property (readonly, strong, nullable) IRCClient *client; @property (readonly, copy, nullable) NSString *clientId; @end @protocol TDCChannelPrototype @required @property (readonly, strong, nullable) IRCChannel *channel; @property (readonly, copy, nullable) NSString *channelId; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TDCWelcomeSheetPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCSheetBase.h" NS_ASSUME_NONNULL_BEGIN @class IRCClientConfig; @interface TDCWelcomeSheet : TDCSheetBase - (void)start; @end @protocol TDCWelcomeSheetDelegate @required - (void)welcomeSheet:(TDCWelcomeSheet *)sender onOk:(IRCClientConfig *)config; - (void)welcomeSheetWillClose:(TDCWelcomeSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/THOPluginDispatcherPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCCommandIndex.h" #import "TVCLogLine.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel, IRCPrefix, IRCMessage; @class TVCLogController; @class THOPluginDidPostNewMessageConcreteObject; @class THOPluginWebViewJavaScriptPayloadConcreteObject; @interface THOPluginDispatcher : NSObject + (dispatch_queue_t)dispatchQueue; + (BOOL)receivedCommand:(NSString *)command withText:(nullable NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt referenceMessage:(nullable IRCMessage *)referenceMessage; + (BOOL)receivedText:(NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination asLineType:(TVCLogLineType)lineType onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt wasEncrypted:(BOOL)wasEncrypted; + (nullable IRCMessage *)interceptServerInput:(IRCMessage *)inputObject for:(IRCClient *)client; + (nullable id)interceptUserInput:(id)inputObject command:(IRCRemoteCommand)commandString; + (NSString *)willRenderMessage:(NSString *)newMessage forViewController:(TVCLogController *)viewController lineType:(TVCLogLineType)lineType memberType:(TVCLogLineMemberType)memberType; + (void)userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString; + (void)didReceiveJavaScriptPayload:(THOPluginWebViewJavaScriptPayloadConcreteObject *)payloadObject fromViewController:(TVCLogController *)viewController; + (void)didReceiveServerInput:(IRCMessage *)inputObject onClient:(IRCClient *)client; + (void)enqueueDidPostNewMessage:(THOPluginDidPostNewMessageConcreteObject *)messageObject; + (void)dequeueDidPostNewMessageWithLineNumber:(NSString *)messageLineNumber forViewController:(TVCLogController *)viewController; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/THOPluginItemPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class THOPluginOutputSuppressionRule; typedef NS_OPTIONS(NSUInteger, THOPluginItemSupportedFeature) { THOPluginItemSupportedFeatureDidReceiveCommandEvent = 1 << 1, THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent = 1 << 2, // THOPluginItemSupportedFeatureInlineMediaManipulation = 1 << 3, THOPluginItemSupportedFeatureNewMessagePostedEvent = 1 << 4, THOPluginItemSupportedFeatureOutputSuppressionRules = 1 << 5, THOPluginItemSupportedFeaturePreferencePane = 1 << 6, THOPluginItemSupportedFeatureServerInputDataInterception = 1 << 7, THOPluginItemSupportedFeatureSubscribedServerInputCommands = 1 << 8, THOPluginItemSupportedFeatureSubscribedUserInputCommands = 1 << 9, THOPluginItemSupportedFeatureUserInputDataInterception = 1 << 10, THOPluginItemSupportedFeatureWebViewJavaScriptPayloads = 1 << 11, THOPluginItemSupportedFeatureWillRenderMessageEvent = 1 << 12, }; @interface THOPluginItem : NSObject @property (readonly, nullable) NSBundle *bundle; @property (readonly, nullable) id primaryClass; @property (readonly, assign) THOPluginItemSupportedFeature supportedFeatures; @property (readonly, copy, nullable) NSArray *supportedServerInputCommands; @property (readonly, copy, nullable) NSArray *supportedUserInputCommands; @property (readonly, copy, nullable) NSArray *outputSuppressionRules; @property (readonly, copy, nullable) NSString *pluginPreferencesPaneMenuItemTitle; @property (readonly, nullable) NSView *pluginPreferencesPaneView; - (BOOL)loadBundle:(NSBundle *)bundle; - (void)unloadBundle; - (BOOL)supportsFeature:(THOPluginItemSupportedFeature)feature; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/THOPluginManagerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "THOPluginItemPrivate.h" NS_ASSUME_NONNULL_BEGIN @class THOPluginOutputSuppressionRule; TEXTUAL_EXTERN NSNotificationName const THOPluginManagerFinishedLoadingPluginsNotification; @interface THOPluginManager : NSObject - (void)loadPlugins; - (void)unloadPlugins; @property (readonly) BOOL pluginsLoaded; @property (readonly, copy, nullable) NSArray *loadedPlugins; // nil until load completes @property (readonly, copy) NSArray *supportedServerInputCommands; @property (readonly, copy) NSArray *supportedUserInputCommands; @property (readonly, copy) NSArray *supportedAppleScriptCommands; @property (readonly, copy) NSDictionary *supportedAppleScriptCommandsAndPaths; @property (readonly, copy) NSArray *pluginsWithPreferencePanes; @property (readonly, copy) NSArray *pluginOutputSuppressionRules; /* Returns YES if at least one loaded plugin supports the feature */ - (BOOL)supportsFeature:(THOPluginItemSupportedFeature)feature; - (void)findHandlerForOutgoingCommand:(NSString *)command path:(NSString * _Nullable * _Nullable)path isReserved:(BOOL *)isReserved isScript:(BOOL *)isScript isExtension:(BOOL *)isExtension; - (void)extrasInstallerAskUserIfTheyWantToInstallCommand:(NSString *)command; - (void)extrasInstallerLaunchInstaller; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/THOPluginProtocolPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCLogLine.h" #import "THOPluginProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface THOPluginDidPostNewMessageConcreteObject () @property (nonatomic, assign, readwrite) BOOL isProcessedInBulk; @property (nonatomic, copy, readwrite) NSString *messageContents; @property (nonatomic, copy, readwrite) NSString *lineNumber; @property (nonatomic, copy, readwrite, nullable) NSString *senderNickname; @property (nonatomic, assign, readwrite) TVCLogLineType lineType; @property (nonatomic, assign, readwrite) TVCLogLineMemberType memberType; @property (nonatomic, copy, readwrite) NSDate *receivedAt; @property (nonatomic, copy, readwrite) NSArray *listOfHyperlinks; @property (nonatomic, copy, readwrite) NSSet *listOfUsers; @property (nonatomic, assign, readwrite) BOOL keywordMatchFound; @end #pragma mark - @interface THOPluginDidReceiveServerInputConcreteObject () @property (nonatomic, assign, readwrite) BOOL senderIsServer; @property (nonatomic, copy, readwrite) NSString *senderNickname; @property (nonatomic, copy, readwrite, nullable) NSString *senderUsername; @property (nonatomic, copy, readwrite, nullable) NSString *senderAddress; @property (nonatomic, copy, readwrite) NSString *senderHostmask; @property (nonatomic, copy, readwrite) NSDate *receivedAt; @property (nonatomic, copy, readwrite) NSString *messageSequence; @property (nonatomic, copy, readwrite) NSArray *messageParameters; @property (nonatomic, copy, readwrite) NSArray *messageParamaters; @property (nonatomic, copy, readwrite) NSString *messageCommand; @property (nonatomic, assign, readwrite) NSUInteger messageCommandNumeric; @property (nonatomic, copy, readwrite, nullable) NSString *networkAddress; @property (nonatomic, copy, readwrite, nullable) NSString *networkName; @end #pragma mark - @interface THOPluginWebViewJavaScriptPayloadConcreteObject () @property (nonatomic, copy, readwrite) NSString *payloadLabel; @property (nonatomic, copy, readwrite, nullable) id payloadContents; @end #pragma mark - @interface NSObject (THOPluginProtocolExtension); - (BOOL)receivedCommand:(NSString *)command withText:(nullable NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt referenceMessage:(nullable IRCMessage *)referenceMessage; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLOEncryptionManagerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "TLOEncryptionManager.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, TVCMainWindowTitlebarAccessoryViewLockButton; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 #define sharedEncryptionManager() [TXSharedApplication sharedEncryptionManager] #endif #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 @interface TLOEncryptionManager : NSObject /* Returns unique "account name" used for messageFrom and messageTo parameters */ - (NSString *)accountNameForUser:(NSString *)nickname onClient:(IRCClient *)client; /* Converts the "account name" into its individual components */ - (nullable NSString *)nicknameFromAccountName:(NSString *)accountName; - (nullable IRCClient *)connectionFromAccountName:(NSString *)accountName; /* Begin and end an encrypted conversation with a user */ - (void)beginConversationWith:(NSString *)messageTo from:(NSString *)messageFrom; - (void)refreshConversationWith:(NSString *)messageTo from:(NSString *)messageFrom; - (void)endConversationWith:(NSString *)messageTo from:(NSString *)messageFrom; /* Socialist Millionaire Problem */ - (void)authenticateUser:(NSString *)messageTo from:(NSString *)messageFrom; /* Open dialog containing list of fingerprints */ - (void)presentListOfFingerprints; /* State information */ - (OTRKitMessageState)messageStateFor:(NSString *)messageTo from:(NSString *)messageFrom; - (void)updateLockIconButton:(TVCMainWindowTitlebarAccessoryViewLockButton *)button withStateOf:(NSString *)messageTo from:(NSString *)messageFrom; - (BOOL)validateMenuItem:(NSMenuItem *)menuItem withStateOf:(NSString *)messageTo from:(NSString *)messageFrom; - (BOOL)safeToTransferFile:(NSString *)filename to:(NSString *)messageTo from:(NSString *)messageFrom isIncomingFileTransfer:(BOOL)isIncomingFileTransfer; /* Define configuration options */ - (void)updatePolicy; /* Encryption/Decryption */ - (void)encryptMessage:(NSString *)messageBody from:(NSString *)messageFrom to:(NSString *)messageTo encodingCallback:(nullable TLOEncryptionManagerEncodingDecodingCallbackBlock)encodingCallback injectionCallback:(nullable TLOEncryptionManagerInjectCallbackBlock)injectionCallback; - (void)decryptMessage:(NSString *)messageBody from:(NSString *)messageFrom to:(NSString *)messageTo decodingCallback:(nullable TLOEncryptionManagerEncodingDecodingCallbackBlock)decodingCallback; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLOFileLoggerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel, IRCTreeItem, TVCLogLine; TEXTUAL_EXTERN NSString * const TLOFileLoggerConsoleDirectoryName; TEXTUAL_EXTERN NSString * const TLOFileLoggerChannelDirectoryName; TEXTUAL_EXTERN NSString * const TLOFileLoggerPrivateMessageDirectoryName; TEXTUAL_EXTERN NSString * const TLOFileLoggerUndefinedNicknameFormat; TEXTUAL_EXTERN NSString * const TLOFileLoggerActionNicknameFormat; TEXTUAL_EXTERN NSString * const TLOFileLoggerNoticeNicknameFormat; TEXTUAL_EXTERN NSString * const TLOFileLoggerISOStandardClockFormat; @interface TLOFileLogger : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithClient:(IRCClient *)client NS_DESIGNATED_INITIALIZER; - (instancetype)initWithChannel:(IRCChannel *)channel NS_DESIGNATED_INITIALIZER; - (void)open; - (void)reopen; - (void)reopenIfNeeded; - (void)close; - (void)reset; // does nothing if file isn't open - (void)writeLogLine:(TVCLogLine *)logLine; - (void)writePlainText:(NSString *)string; #pragma mark - #pragma mark Paths /* All path information is nullable because path information is not cached until log file is opened */ /* File path is the path to the log file for the day */ /* At the time of a write after midnight, this path can be to a log file for the previous date. TLOFileLogger will correct that before writing. Be aware this is the case though if you access this property after midnight and before another write occurs. Unless a specific reason is needed to access this property, it may be better to use -writePath instead. */ @property (readonly, copy, nullable) NSString *filePath; /* The last path component (file name) of file path */ @property (readonly, copy, nullable) NSString *fileName; /* Write path is the path to the folder in which log files for the channel or client are written to. */ @property (readonly, copy, nullable) NSString *writePath; /* -writePathForItem: only returns nil when there is no log location configured in Preferences. */ + (nullable NSString *)writePathForItem:(IRCTreeItem *)item; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLOInputHistoryPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCTreeItem, TVCMainWindow; @interface TLOInputHistory : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithWindow:(TVCMainWindow *)mainWindow NS_DESIGNATED_INITIALIZER; - (void)moveFocusTo:(IRCTreeItem *)treeItem; - (void)destroy:(IRCTreeItem *)treeItem; - (void)noteInputHistoryObjectScopeDidChange; - (void)add:(NSAttributedString *)string; - (nullable NSAttributedString *)up:(NSAttributedString *)string; - (nullable NSAttributedString *)down:(NSAttributedString *)string; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLOLicenseManagerDownloaderPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TLOLicenseManagerDownloaderRequestType) { TLOLicenseManagerDownloaderRequestTypeActivation, TLOLicenseManagerDownloaderRequestTypeMigrateAppStore, TLOLicenseManagerDownloaderRequestTypeSendLostLicense, TLOLicenseManagerDownloaderRequestTypeLicenseUpgradeEligibility, TLOLicenseManagerDownloaderRequestTypeReceiptUpgradeEligibility }; TEXTUAL_EXTERN NSUInteger const TLOLicenseManagerDownloaderRequestStatusCodeSuccess; TEXTUAL_EXTERN NSUInteger const TLOLicenseManagerDownloaderRequestStatusCodeGenericError; TEXTUAL_EXTERN NSUInteger const TLOLicenseManagerDownloaderRequestStatusCodeServiceIsBusy; typedef BOOL (^TLOLicenseManagerDownloaderActionBlock)(NSUInteger statusCode, id _Nullable statusContext); typedef BOOL (^TLOLicenseManagerDownloaderErrorBlock)(NSUInteger statusCode, id _Nullable statusContext); typedef void (^TLOLicenseManagerDownloaderCompletionBlock)(BOOL resultSuccessful, NSUInteger statusCode, id _Nullable statusContext); @interface TLOLicenseManagerDownloader : NSObject @property (nonatomic, copy, nullable) TLOLicenseManagerDownloaderActionBlock actionBlock; @property (nonatomic, copy, nullable) TLOLicenseManagerDownloaderActionBlock errorBlock; @property (nonatomic, copy, nullable) TLOLicenseManagerDownloaderCompletionBlock completionBlock; @property (nonatomic, assign) BOOL isSilentOnFailure; @property (nonatomic, assign) BOOL isSilentOnSuccess; - (void)activateLicense:(NSString *)licenseKey; - (void)deactivateLicense; - (void)checkUpgradeEligibilityOfLicense:(NSString *)licenseKey; - (void)checkUpgradeEligibilityOfReceipt:(NSString *)receiptData; - (void)requestLostLicenseKeyForContactAddress:(NSString *)contactAddress; - (void)migrateMacAppStorePurchase:(NSString *)receiptData licenseOwnerName:(NSString *)licenseOwnerName licenseOwnerContactAddress:(NSString *)licenseOwnerContactAddress; - (void)cancelRequest; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLOLicenseManagerLastGenPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN /* TLOLicenseManagerLastGen is a class because classes are easier to work with. TLOLicenseManager* functions are functions because a plugin can't override the logic of the functions as easily. There is no risk in security if TLOLicenseManagerLastGen is swizzled. */ /* TLOLicenseManagerLastGen processes the old location for license file and returns license key from it if it is not the current generation. The class is also capable of doing the same for a data object so that the activation system can know whether a license key is old. */ #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @interface TLOLicenseManagerLastGen : NSObject /* Only returns a value if license key is old. */ + (nullable NSString *)licenseKey; // From saved license file (last gen location) + (nullable NSString *)licenseKeyForLicenseContents:(NSData *)licenseContents; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLOLicenseManagerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TLOLicenseManagerActionResult) { TLOLicenseManagerActionResultSuccess, // Validated successfully and saved TLOLicenseManagerActionResultGenerationPrevious, // License is for previous version. Discount available? TLOLicenseManagerActionResultGenerationNext, // License is for next version. Maybe they should update. TLOLicenseManagerActionResultInvalidSignature, // License signature is invalid TLOLicenseManagerActionResultCannotRead, // License cannot be read from disk TLOLicenseManagerActionResultCannotWrite, // License cannot be written to disk TLOLicenseManagerActionResultMalformedData, // License data is not correct or complete TLOLicenseManagerActionResultOther // Other failure reason }; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 typedef NSString *TLOLicenseManagerLicenseDictionaryKey NS_EXTENSIBLE_STRING_ENUM; TEXTUAL_EXTERN TLOLicenseManagerLicenseDictionaryKey const TLOLicenseManagerLicenseDictionaryKeyCreationDate; TEXTUAL_EXTERN TLOLicenseManagerLicenseDictionaryKey const TLOLicenseManagerLicenseDictionaryKeyGeneration; TEXTUAL_EXTERN TLOLicenseManagerLicenseDictionaryKey const TLOLicenseManagerLicenseDictionaryKeyLicenseKey; TEXTUAL_EXTERN TLOLicenseManagerLicenseDictionaryKey const TLOLicenseManagerLicenseDictionaryKeyProductName; TEXTUAL_EXTERN TLOLicenseManagerLicenseDictionaryKey const TLOLicenseManagerLicenseDictionaryKeyOwnerContactAddress; TEXTUAL_EXTERN TLOLicenseManagerLicenseDictionaryKey const TLOLicenseManagerLicenseDictionaryKeyOwnerName; TEXTUAL_EXTERN TLOLicenseManagerLicenseDictionaryKey const TLOLicenseManagerLicenseDictionaryKeySignature; TEXTUAL_EXTERN NSUInteger const TLOLicenseManagerCurrentLicenseGeneration; TEXTUAL_EXTERN void TLOLicenseManagerSetup(void); TEXTUAL_EXTERN BOOL TLOLicenseManagerTextualIsRegistered(void); TEXTUAL_EXTERN BOOL TLOLicenseManagerIsTrialExpired(void); TEXTUAL_EXTERN NSTimeInterval TLOLicenseManagerTimeRemainingTrial(void); TEXTUAL_EXTERN TLOLicenseManagerActionResult TLOLicenseManagerDeleteLicenseFile(void); TEXTUAL_EXTERN TLOLicenseManagerActionResult TLOLicenseManagerWriteLicenseFileContents(NSData * _Nullable newContents); TEXTUAL_EXTERN BOOL TLOLicenseManagerLicenseKeyIsValid(NSString *licenseKey); TEXTUAL_EXTERN NSString * _Nullable TLOLicenseManagerLicenseCreationDate(void); TEXTUAL_EXTERN NSString * _Nullable TLOLicenseManagerLicenseCreationDateFormatted(void); TEXTUAL_EXTERN NSUInteger TLOLicenseManagerLicenseGeneration(void); TEXTUAL_EXTERN NSString * _Nullable TLOLicenseManagerLicenseKey(void); TEXTUAL_EXTERN NSString * _Nullable TLOLicenseManagerLicenseOwnerContactAddress(void); TEXTUAL_EXTERN NSString * _Nullable TLOLicenseManagerLicenseOwnerName(void); TEXTUAL_EXTERN NSString * TLOLicenseManagerAuthorizationCode(void); #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLONicknameCompletionStatusPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCMainWindow; @interface TLONicknameCompletionStatus : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithWindow:(TVCMainWindow *)mainWindow NS_DESIGNATED_INITIALIZER; - (void)completeNickname:(BOOL)movingForward; - (void)clear; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLONotificationConfigurationPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLONotificationController.h" NS_ASSUME_NONNULL_BEGIN typedef NSString *TLONotificationAlertSound NS_EXTENSIBLE_STRING_ENUM; TEXTUAL_EXTERN TLONotificationAlertSound const TXDefaultAlertSoundPreferenceValue; TEXTUAL_EXTERN TLONotificationAlertSound const TXNoAlertSoundPreferenceValue; @interface TLONotificationConfiguration : NSObject @property (readonly) TXNotificationType eventType; @property (readonly, copy) NSString *displayName; @property (nonatomic, copy, nullable) TLONotificationAlertSound alertSound; @property (nonatomic, assign) NSUInteger speakEvent; @property (nonatomic, assign) NSUInteger pushNotification; @property (nonatomic, assign) NSUInteger disabledWhileAway; @property (nonatomic, assign) NSUInteger bounceDockIcon; @property (nonatomic, assign) NSUInteger bounceDockIconRepeatedly; + (NSString *)localizedAlertDefaultSoundTitle; + (NSString *)localizedAlertNoSoundTitle; + (instancetype)configurationWithEventType:(TXNotificationType)eventType; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithEventType:(TXNotificationType)aEventType NS_DESIGNATED_INITIALIZER; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLONotificationControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLONotificationController.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient; TEXTUAL_EXTERN NSString * const TXNotificationUserInfoClientIdentifierKey; TEXTUAL_EXTERN NSString * const TXNotificationUserInfoChannelIdentifierKey; TEXTUAL_EXTERN NSString * const TXNotificationDialogStandardNicknameFormat; TEXTUAL_EXTERN NSString * const TXNotificationDialogActionNicknameFormat; TEXTUAL_EXTERN NSString * const TXNotificationHighlightLogStandardActionFormat; TEXTUAL_EXTERN NSString * const TXNotificationHighlightLogStandardMessageFormat; @interface TLONotificationController () /* All methods in this controller do not honor any user preference for silencing notifications. By the time a notification reaches this point, it is assumed that those related conditions have been checked. */ /* This method will automatically configure the notification based on the event type such as setting a title or description. It will also perform formatter stripping if need be. In addition to properly separating notifications by threads. */ - (void)notify:(TXNotificationType)eventType title:(nullable NSString *)eventTitle description:(nullable NSString *)eventDescription userInfo:(nullable NSDictionary *)eventContext; - (void)dismissNotificationsForChannel:(nullable IRCChannel *)channel onClient:(IRCClient *)client; /* These methods schedule notifications with the UserNotification.framework. Nothing more. -notify:title:description:userInfo: is the proper entry point for sending notifications related to IRC. These entry points are conveniences for sending unrelated notifications such as from the license manager or addons. */ - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message onClient:(IRCClient *)client; - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message forChannel:(IRCChannel *)channel; - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message forChannel:(nullable IRCChannel *)channel onClient:(IRCClient *)client; - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message userInfo:(nullable NSDictionary *)userInfo; - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message userInfo:(nullable NSDictionary *)userInfo threadIdentifier:(NSString *)threadIdentifier; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLOSpeechSynthesizerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient; @interface TLOSpeechSynthesizer : NSObject /* If stopped, any items supplied to speak: will be completely ignored. */ /* Setting the synthesizer to stopped does not clear the queue. Just forces speaking to stop and does not allow additions. */ @property (nonatomic, assign) BOOL isStopped; - (void)speak:(id)object; // NSString or TLOSpokeNotification - (void)clearQueue; // Does not stop speaking. Only clears pending items. - (void)clearQueueForClient:(IRCClient *)client; - (void)stopSpeakingAndMoveForward; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TLOSpokenNotificationPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLONotificationController.h" #import "TVCLogLine.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel, IRCTreeItem; @interface TLOSpokenNotification : NSObject @property (nonatomic, weak, null_unspecified, readonly) IRCClient *client; @property (nonatomic, weak, null_unspecified, readonly) IRCChannel *channel; @property (nonatomic, copy, null_unspecified, readonly) NSString *nickname; @property (nonatomic, copy, null_unspecified, readonly) NSString *text; @property (nonatomic, readonly) TVCLogLineType lineType; @property (nonatomic, readonly) TXNotificationType notificationType; - (instancetype)initWithNotification:(TXNotificationType)notificationType lineType:(TVCLogLineType)lineType target:(null_unspecified IRCTreeItem *)target nickname:(null_unspecified NSString *)nickname text:(null_unspecified NSString *)text; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TPCApplicationInfoPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCApplicationInfo.h" NS_ASSUME_NONNULL_BEGIN @interface TPCApplicationInfo () + (void)saveTimeIntervalSinceApplicationInstall; + (void)incrementApplicationRunCount; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TPCPathInfoPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPathInfo.h" NS_ASSUME_NONNULL_BEGIN @interface TPCPathInfo () + (void)_createDirectoryAtPath:(NSString *)directoryPath; + (void)_createDirectoryAtURL:(NSURL *)directoryURL; @end @interface TPCPathInfo (TPCPathInfoTranscriptFolderExtension) @property (class, readonly, copy, nullable) NSString *transcriptFolder; @property (class, readonly, copy, nullable) NSURL *transcriptFolderURL; + (void)setTranscriptFolderURL:(nullable NSData *)transcriptFolderURL; + (void)startUsingTranscriptFolderURL; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TPCPreferencesImportExportPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferencesImportExport.h" NS_ASSUME_NONNULL_BEGIN @interface TPCPreferencesImportExport () + (void)import:(id)object withKey:(NSString *)key; + (void)importContentsOfDictionary:(NSDictionary *)aDict; + (void)importClientConfiguration:(NSDictionary *)config; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TPCPreferencesLocalPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferencesPrivate.h" #import "TPCPreferencesLocal.h" NS_ASSUME_NONNULL_BEGIN @interface TPCPreferences (TPCPreferencesLocalPrivate) + (void)initPreferences; + (void)setAppNapEnabled:(BOOL)appNapEnabled; + (void)setDeveloperModeEnabled:(BOOL)developerModeEnabled; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 + (void)setTextEncryptionIsOpportunistic:(BOOL)textEncryptionIsOpportunistic; + (void)setTextEncryptionIsRequired:(BOOL)textEncryptionIsRequired; + (void)setTextEncryptionIsEnabled:(BOOL)textEncryptionIsEnabled; #endif + (void)setLogToDisk:(BOOL)logToDisk; + (void)setOnlySpeakEventsForSelection:(BOOL)onlySpeakEventsForSelection; + (void)setChannelMessageSpeakChannelName:(BOOL)channelMessageSpeakChannelName; + (void)setChannelMessageSpeakNickname:(BOOL)channelMessageSpeakNickname; + (void)setHighlightCurrentNickname:(BOOL)highlightCurrentNickname; + (void)setLocationToSendNotices:(TXNoticeSendLocation)locationToSendNotices; + (void)setAppearance:(TXPreferredAppearance)appearance; + (void)setThemeName:(NSString *)value; + (void)setThemeNameWithExistenceCheck:(NSString *)value; + (void)setThemeChannelViewFontName:(NSString *)value; + (void)setThemeChannelViewFontNameWithExistenceCheck:(NSString *)value; + (void)setThemeChannelViewFontSize:(CGFloat)value; + (void)setThemeNicknameFormatPreferenceUserConfigurable:(BOOL)themeNicknameFormatPreferenceUserConfigurable; + (void)setThemeTimestampFormatPreferenceUserConfigurable:(BOOL)themeTimestampFormatPreferenceUserConfigurable; + (void)setThemeChannelViewFontPreferenceUserConfigurable:(BOOL)themeChannelViewFontPreferenceUserConfigurable; + (void)setThemeUserStyleSheetRules:(nullable NSString *)themeUserStyleSheetRules; + (void)setScrollbackSaveLimit:(NSUInteger)scrollbackSaveLimit; + (void)setScrollbackVisibleLimit:(NSUInteger)scrollbackVisibleLimit; + (void)setShowInlineMedia:(BOOL)showInlineMedia; + (void)setSoundIsMuted:(BOOL)soundIsMuted; + (void)setSound:(nullable NSString *)value forEvent:(TXNotificationType)event; + (void)setNotificationEnabled:(BOOL)value forEvent:(TXNotificationType)event; + (void)setDisabledWhileAway:(BOOL)value forEvent:(TXNotificationType)event; + (void)setBounceDockIcon:(BOOL)value forEvent:(TXNotificationType)event; + (void)setBounceDockIconRepeatedly:(BOOL)value forEvent:(TXNotificationType)event; + (void)setEventIsSpoken:(BOOL)value forEvent:(TXNotificationType)event; + (nullable NSString *)keyForEvent:(TXNotificationType)event category:(NSString *)category; + (void)setFileTransferPortRangeStart:(uint16_t)value; + (void)setFileTransferPortRangeEnd:(uint16_t)value; + (void)setTabCompletionSuffix:(NSString *)value; + (void)setClientList:(nullable NSArray *)clientList; + (void)cleanUpHighlightKeywords; + (void)setTextFieldAutomaticSpellCheck:(BOOL)value; + (void)setTextFieldAutomaticGrammarCheck:(BOOL)value; + (void)setTextFieldAutomaticSpellCorrection:(BOOL)value; + (void)setTextFieldSmartCopyPaste:(BOOL)value; + (void)setTextFieldSmartQuotes:(BOOL)value; + (void)setTextFieldSmartDashes:(BOOL)value; + (void)setTextFieldSmartLinks:(BOOL)value; + (void)setTextFieldDataDetectors:(BOOL)value; + (void)setTextFieldTextReplacement:(BOOL)value; + (void)setWebKit2Enabled:(BOOL)webKit2Enabled; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TPCResourceManagerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCResourceManager.h" NS_ASSUME_NONNULL_BEGIN @interface TPCResourceManager () + (void)copyResourcesToApplicationSupportFolder; @end @interface TPCResourceManagerDocumentTypeImporter : NSDocument @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TPCSandboxMigrationPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2024 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCResourceManager.h" NS_ASSUME_NONNULL_BEGIN @interface TPCSandboxMigration : NSObject + (void)migrateResources; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TPCThemeControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCThemeController.h" NS_ASSUME_NONNULL_BEGIN @interface TPCThemeController () /* -load is called when app first launches to do first load with some extra safety checks. Use -reload if you want to reload the theme. */ - (void)load; - (void)reload; - (void)recreateTemporaryCopyOfThemeIfNecessary; - (void)copyActiveThemeToDestinationLocation:(TPCThemeStorageLocation)destinationLocation reloadOnCopy:(BOOL)reloadOnCopy openOnCopy:(BOOL)openOnCopy; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TPCThemePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCTheme.h" NS_ASSUME_NONNULL_BEGIN @interface TPCTheme () - (instancetype)initWithURL:(NSURL *)url inStorageLocation:(TPCThemeStorageLocation)storageLocation NS_DESIGNATED_INITIALIZER; @property (readonly, copy) NSString *applicationTemplateRepositoryPath; - (void)updateAppearance; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCAppearancePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCAppearance.h" NS_ASSUME_NONNULL_BEGIN @interface TVCAppearance () /* TVCListAppearance will take care of all the hard work such as inheritance and retina. You only need to specify "BigSurDark" for example, not "BigSurDarkRetina" */ - (nullable instancetype)initWithAppearanceNamed:(NSString *)appearanceName atURL:(NSURL *)appearanceLocation forRetinaDisplay:(BOOL)forRetinaDisplay NS_DESIGNATED_INITIALIZER; /* When a subclass finishes applying all appearance values to properties, it can flush the top level group which will cause it to disappear from memory, thus reducing overall memory use. */ - (void)flushAppearanceProperties; @end @interface TVCApplicationAppearance () /* Appearance name is inherited from TXApplication */ - (nullable instancetype)initWithAppearanceAtURL:(NSURL *)appearanceLocation forRetinaDisplay:(BOOL)forRetinaDisplay NS_DESIGNATED_INITIALIZER; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCChannelSelectionOutlineViewCellPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCChannelSelectionViewController; @interface TVCChannelSelectionOutlineCellView : NSTableCellView @property (nonatomic, weak) TVCChannelSelectionViewController *parentController; @property (nonatomic, weak) IBOutlet NSButton *selectedCheckbox; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCChannelSelectionViewControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCChannelSelectionViewController.h" NS_ASSUME_NONNULL_BEGIN @class TVCChannelSelectionOutlineCellView; @interface TVCChannelSelectionViewController () - (NSOutlineView *)outlineView; - (void)selectionCheckboxClickedInCell:(TVCChannelSelectionOutlineCellView *)clickedCell; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCContentNavigationOutlineViewPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCContentNavigationOutlineViewItem; @interface TVCContentNavigationOutlineView : NSOutlineView @property (nonatomic, assign) NSUInteger contentViewPreferredWidth; @property (nonatomic, assign) NSUInteger contentViewPreferredHeight; @property (nonatomic, copy) NSArray *navigationTreeMatrix; @property (nonatomic, assign) BOOL expandParentOnDoubleClick; @property (readonly, weak, nullable) TVCContentNavigationOutlineViewItem *selectedItem; - (void)navigateToItemWithIdentifier:(NSUInteger)identifier; @end @interface TVCContentNavigationOutlineViewItem : NSObject @property (readonly, copy) NSString *label; @property (readonly) NSUInteger identifier; @property (readonly, weak, nullable) NSView *view; @property (readonly, weak, nullable) NSControl *firstResponder; @property (readonly, copy, nullable) NSArray *children; @property (readonly) BOOL isGroupItem; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithLabel:(NSString *)label identifier:(NSUInteger)identifier view:(NSView *)view firstResponder:(nullable NSControl *)firstResponder; /* view and children cannot both be nil or an exception is raised */ - (instancetype)initWithLabel:(NSString *)label identifier:(NSUInteger)identifier view:(nullable NSView *)view firstResponder:(nullable NSControl *)firstResponder children:(nullable NSArray *)children NS_DESIGNATED_INITIALIZER; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCDockIconPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCDockIcon : NSObject + (void)updateDockIcon; + (void)resetCachedCount; + (void)drawWithoutCount; + (void)drawWithHighlightCount:(NSUInteger)highlightCount messageCount:(NSUInteger)messageCount; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCErrorMessagePopoverControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCErrorMessagePopoverController : NSObject + (TVCErrorMessagePopoverController *)sharedController; - (void)showMessage:(NSString *)message forView:(NSView *)view; - (void)closeMessage; - (void)closeMessageForView:(NSView *)view; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCErrorMessagePopoverPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @protocol TVCErrorMessagePopoverDelegate; @interface TVCErrorMessagePopover : NSObject @property (nonatomic, weak) id delegate; @property (readonly, copy) NSString *message; @property (readonly, weak) NSView *view; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithMessage:(NSString *)message relativeToView:(NSView *)view NS_DESIGNATED_INITIALIZER; - (void)showRelativeToRect:(NSRect)rect; - (void)showRelativeToRect:(NSRect)rect preferredEdge:(NSRectEdge)preferredEdge; - (void)close; @end @protocol TVCErrorMessagePopoverDelegate @optional - (void)errorMessagePopoverWillShow:(TVCErrorMessagePopover *)popover; - (void)errorMessagePopoverDidShow:(TVCErrorMessagePopover *)popover; - (void)errorMessagePopoverWillClose:(TVCErrorMessagePopover *)popover; - (void)errorMessagePopoverDidClose:(TVCErrorMessagePopover *)popover; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogControllerHistoricLogFilePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCTreeItem, TVCLogLine; #define TVCLogControllerHistoricLogSharedInstance() [TVCLogControllerHistoricLogFile sharedInstance] @interface TVCLogControllerHistoricLogFile : NSObject + (TVCLogControllerHistoricLogFile *)sharedInstance; - (void)writeNewEntryWithLogLine:(TVCLogLine *)logLine forItem:(IRCTreeItem *)item; - (void)saveData; // asynchronous operation - (void)resetMaximumLineCount; @property (readonly) BOOL isSaving; - (void)forgetItem:(IRCTreeItem *)item; - (void)resetDataForItem:(IRCTreeItem *)item; - (void)fetchEntriesForItem:(IRCTreeItem *)item ascending:(BOOL)ascending fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (^)(NSArray *entries))completionBlock; - (void)fetchEntriesForItem:(IRCTreeItem *)item withUniqueIdentifier:(NSString *)uniqueId beforeFetchLimit:(NSUInteger)fetchLimitBefore afterFetchLimit:(NSUInteger)fetchLimitAfter limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (^)(NSArray *entries))completionBlock; - (void)fetchEntriesForItem:(IRCTreeItem *)item beforeUniqueIdentifier:(NSString *)uniqueId fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (^)(NSArray *entries))completionBlock; - (void)fetchEntriesForItem:(IRCTreeItem *)item afterUniqueIdentifier:(NSString *)uniqueId fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (^)(NSArray *entries))completionBlock; - (void)fetchEntriesForItem:(IRCTreeItem *)item afterUniqueIdentifier:(NSString *)uniqueIdAfter beforeUniqueIdentifier:(NSString *)uniqueIdBefore fetchLimit:(NSUInteger)fetchLimit withCompletionBlock:(void (^)(NSArray *entries))completionBlock; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogControllerInlineMediaServicePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCTreeItem; #define TVCLogControllerInlineMediaSharedInstance() [TVCLogControllerInlineMediaService sharedInstance] @interface TVCLogControllerInlineMediaService : NSObject + (TVCLogControllerInlineMediaService *)sharedInstance; - (void)processAddress:(NSString *)address withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index forItem:(IRCTreeItem *)item; - (void)reloadService; /* This will present a modal alert asking user for permission to enable inline media so that they are aware of the risk of IP address leaks. Completion block returns YES on permission granted. NO in all other cases. */ + (void)askPermissionToEnableInlineMediaWithCompletionBlock:(void (NS_NOESCAPE ^)(BOOL granted))completionBlock; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogControllerOperationQueuePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel, TVCLogController; typedef void (^TVCLogControllerPrintingBlock)(NSOperation *sender); @interface TVCLogControllerPrintingOperationQueue : NSOperationQueue - (void)enqueueMessageBlock:(TVCLogControllerPrintingBlock)callbackBlock for:(TVCLogController *)viewController; - (void)enqueueMessageBlock:(TVCLogControllerPrintingBlock)callbackBlock for:(TVCLogController *)viewController isStandalone:(BOOL)isStandalone; - (void)cancelOperationsForClient:(IRCClient *)client; - (void)cancelOperationsForChannel:(IRCChannel *)channel; - (void)cancelOperationsForViewController:(TVCLogController *)viewController; - (void)updateReadinessState:(TVCLogController *)viewController; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCLogController.h" NS_ASSUME_NONNULL_BEGIN @class ICLPayload; @interface TVCLogController () @property (nonatomic, assign, readwrite, getter=viewIsEncrypted) BOOL encrypted; - (instancetype)initWithClient:(IRCClient *)client inWindow:(TVCMainWindow *)window NS_DESIGNATED_INITIALIZER; - (instancetype)initWithChannel:(IRCChannel *)channel inWindow:(TVCMainWindow *)window NS_DESIGNATED_INITIALIZER; - (void)notifyDidBecomeHidden; - (void)notifyDidBecomeVisible; - (void)notifySelectionChanged; - (void)changeScrollbackLimit; - (void)clearBackingView; - (void)reloadTheme; - (void)print:(TVCLogLine *)logLine; - (void)print:(TVCLogLine *)logLine completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock; - (void)renderLogLineAtLineNumber:(NSString *)lineNumber numberOfLinesBefore:(NSUInteger)numberOfLinesBefore numberOfLinesAfter:(NSUInteger)numberOfLinesAfter completionBlock:(void (^)(NSArray *> * _Nonnull))completionBlock; - (void)renderLogLinesBeforeLineNumber:(NSString *)lineNumber maximumNumberOfLines:(NSUInteger)maximumNumberOfLines completionBlock:(void (^)(NSArray *> *))completionBlock; - (void)renderLogLinesAfterLineNumber:(NSString *)lineNumber maximumNumberOfLines:(NSUInteger)maximumNumberOfLines completionBlock:(void (^)(NSArray *> *))completionBlock; - (void)renderLogLinesAfterLineNumber:(NSString *)lineNumberAfter beforeLineNumber:(NSString *)lineNumberBefore maximumNumberOfLines:(NSUInteger)maximumNumberOfLines completionBlock:(void (^)(NSArray *> * _Nonnull))completionBlock; - (void)notifyLinesAddedToView:(NSArray *)lineNumbers; - (void)notifyLinesRemovedFromView:(NSArray *)lineNumbers; - (void)notifyJumpToLine:(NSString *)lineNumber successful:(BOOL)successful scrolledToBottom:(BOOL)scrolledToBottom; - (void)notifyHistoricLogWillDeleteLines:(NSArray *)lineNumbers; - (void)processInlineMediaAtAddress:(NSString *)address withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index; - (void)processingInlineMediaPayloadSucceeded:(ICLPayload *)payload; - (void)processingInlineMediaPayload:(ICLPayload *)payload failedWithError:(NSError *)error; - (void)logViewWebViewClosedUnexpectedly; - (void)logViewWebViewFinishedLoading; - (void)logViewWebViewKeyDown:(NSEvent *)e; - (void)logViewWebViewReceivedDropWithFile:(NSString *)filename; - (nullable TVCLogLine *)lastLine; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogLinePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCLogLine.h" NS_ASSUME_NONNULL_BEGIN @class TVCLogLineXPC, IRCTreeItem; @interface TVCLogLine () @property (readonly) BOOL fromCurrentSession; - (NSString *)renderedBodyForTranscriptLog; - (NSString *)renderedBodyForTranscriptLogInChannel:(nullable IRCChannel *)channel; + (TVCLogLine *)logLineFromXPCObject:(TVCLogLineXPC *)xpcObject; - (TVCLogLineXPC *)xpcObjectForTreeItem:(IRCTreeItem *)treeItem; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogPolicyPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCLogView; @interface TVCLogPolicy : NSObject @property (nonatomic, copy, nullable) NSString *anchorURL; @property (nonatomic, copy, nullable) NSString *channelName; @property (nonatomic, copy, nullable) NSString *nickname; - (void)displayContextMenuInWebView:(TVCLogView *)webView; - (void)channelNameDoubleClicked; - (void)nicknameDoubleClicked; - (void)topicBarDoubleClicked; TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN - (NSArray *)webView1:(WebView *)webView logView:(TVCLogView *)logView contextMenuWithDefaultMenuItems:(NSArray *)defaultMenuItems; - (void)webView1:(WebView *)webView logView:(TVCLogView *)logView resource:(id)identifier didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge fromDataSource:(WebDataSource *)dataSource; - (NSUInteger)webView1:(WebView *)webView logView:(TVCLogView *)logView dragDestinationActionMaskForDraggingInfo:(id)draggingInfo; - (void)webView1:(WebView *)webView logView:(TVCLogView *)logView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id )listener; TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END - (void)webView2:(WKWebView *)webView logView:(TVCLogView *)logView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler; - (void)webView2:(WKWebView *)webView logView:(TVCLogView *)logView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler; - (NSMenu *)webView2:(WKWebView *)webView logView:(TVCLogView *)logView contextMenuWithDefaultMenu:(NSMenu *)defaultMenu; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogScriptEventSinkPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCLogView; @interface TVCLogScriptEventSink : NSObject - (instancetype)initWithWebView:(nullable TVCLogView *)webView NS_DESIGNATED_INITIALIZER; + (void)logToJavaScriptConsole:(NSString *)message inWebView:(TVCLogView *)webView, ...; + (void)logToJavaScriptConsole:(NSString *)message inWebView:(TVCLogView *)webView withArguments:(va_list)arguments; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogViewInternalWK1.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN NS_ASSUME_NONNULL_BEGIN @class TVCLogPolicy, TVCLogView; @interface TVCLogViewInternalWK1 : WebView @property (nonatomic, weak) TVCLogView *t_parentView; @property (nonatomic, assign) BOOL t_viewHasLoaded; @property (nonatomic, assign) BOOL t_viewHasScriptObject; - (instancetype)initWithHostView:(TVCLogView *)hostView; - (void)_t_evaluateJavaScript:(NSString *)code completionHandler:(void (^ _Nullable)(id _Nullable))completionHandler; @property (readonly) TVCLogPolicy *webViewPolicy; - (void)findString:(NSString *)searchString movingForward:(BOOL)movingForward; + (void)emptyCaches; @end NS_ASSUME_NONNULL_END TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogViewInternalWK2.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCLogPolicy, TVCLogView; @interface TVCLogViewInternalWK2 : WKWebView /* This flag will be YES unless a WebKit2 processes terminates for an unexpected reason such as a crash or memory exhaustion. In those cases, this property will become NO. The rest of Textual will give it priority over any other user preference asking for asking access to WebKit2. */ @property (readonly, class) BOOL t_safeToUse; @property (nonatomic, weak) TVCLogView *t_parentView; @property (nonatomic, assign) BOOL t_viewIsLoading; @property (nonatomic, assign) BOOL t_viewIsNavigating; - (instancetype)initWithHostView:(TVCLogView *)hostView; - (void)_t_evaluateJavaScript:(NSString *)code completionHandler:(void (^ _Nullable)(id _Nullable))completionHandler; @property (readonly) TVCLogPolicy *webViewPolicy; - (void)findString:(NSString *)searchString movingForward:(BOOL)movingForward; + (void)emptyCaches; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCLogViewPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCLogView.h" NS_ASSUME_NONNULL_BEGIN @class TVCLogController, TVCLogPolicy; @interface TVCLogView () @property (nonatomic, weak) TVCLogController *viewController; @property (readonly) TVCLogPolicy *webViewPolicy; @property (nonatomic, copy, readwrite, nullable) NSString *selection; @property (nonatomic, assign) BOOL viewingBottom; - (instancetype)initWithViewController:(TVCLogController *)viewController NS_DESIGNATED_INITIALIZER; - (void)informDelegateWebViewClosedUnexpectedly; - (void)informDelegateWebViewFinishedLoading; - (void)setViewFinishedLayout; - (BOOL)keyDown:(NSEvent *)e inView:(NSView *)view; - (BOOL)performDragOperation:(id )sender; - (void)copyContentString; - (void)print; @end @interface TVCLogView (TVCLogViewBackingViewProxy) + (void)emptyCaches; - (void)stopLoading; - (void)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL; - (void)findString:(NSString *)searchString movingForward:(BOOL)movingForward; - (void)redrawViewIfNeeded; - (void)redrawView; - (void)resetScrollerPosition; - (void)resetScrollerPositionTo:(BOOL)scrolledToBottom; - (void)saveScrollerPosition; - (void)restoreScrollerPosition; - (void)enableOffScreenUpdates; - (void)disableOffScreenUpdates; - (void)setAutomaticScrollingEnabled:(BOOL)automaticScrollingEnabled; @end @interface TVCLogView (TVCLogViewJavaScriptHandlerPrivate) - (NSString *)compiledFunctionCall:(NSString *)function withArguments:(nullable NSArray *)arguments; TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN - (id)webScriptObjectToCommon:(WebScriptObject *)object; TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowAppearancePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowAppearance.h" NS_ASSUME_NONNULL_BEGIN @class TVCMainWindow; @interface TVCMainWindowAppearance () - (nullable instancetype)initWithWindow:(TVCMainWindow *)mainWindow; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowChannelViewPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowChannelView : NSSplitView - (void)populateSubviews; - (void)updateArrangement; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowLoadingScreenPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowAppearance.h" #import "TVCMainWindowLoadingScreen.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowLoadingScreenView () @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCCommandIndex.h" #import "TVCMainWindow.h" NS_ASSUME_NONNULL_BEGIN @class IRCUser; @class TLOInputHistory; @class TVCMainWindowChannelView; @class TVCMainWindowTitlebarAccessoryView, TVCMainWindowTitlebarAccessoryViewController; @class TVCMainWindowTitlebarAccessoryViewLockButton; @class TVCTextViewIRCFormattingMenu; @class TXMenuControllerMainWindowProxy; typedef NS_OPTIONS(NSUInteger, TVCMainWindowShiftSelectionFlags) { TVCMainWindowShiftSelectionFlagMaintainGrouping = 1 << 0, TVCMainWindowShiftSelectionFlagPerformDeselect = 1 << 1, // deselect previous selection TVCMainWindowShiftSelectionFlagPerformDeselectChildren = 1 << 2, // deselect previous selection + children (if group item) // TVCMainWindowShiftSelectionFlagPerformDeselectAll = 1 << 2 // deselect all }; typedef NS_OPTIONS(NSUInteger, TVCMainWindowMouseLocation) { TVCMainWindowMouseLocationOutsideWindow = 0, TVCMainWindowMouseLocationInsideWindow = 1 << 1, TVCMainWindowMouseLocationInsideWindowTitle = 1 << 2, TVCMainWindowMouseLocationOnTopOfWindowTitleControl = 1 << 3 }; @interface TVCMainWindow () @property (nonatomic, assign) BOOL ignoreOutlineViewSelectionChanges; @property (nonatomic, assign) BOOL ignoreNextOutlineViewSelectionChange; - (TLOInputHistory *)inputHistoryManager; - (TVCMainWindowChannelView *)channelView; - (TVCMainWindowTitlebarAccessoryView *)titlebarAccessoryView; - (nullable TVCMainWindowTitlebarAccessoryViewController *)titlebarAccessoryViewController; - (TVCMainWindowTitlebarAccessoryViewLockButton *)titlebarAccessoryViewLockButton; - (TVCTextViewIRCFormattingMenu *)formattingMenu; - (TXMenuControllerMainWindowProxy *)mainMenuProxy; - (TVCMainWindowMouseLocation)locationOfMouseInWindow; - (TVCMainWindowMouseLocation)locationOfMouse:(NSPoint)mouseLocation; - (BOOL)reloadingTheme; - (BOOL)reloadLoadingScreen; - (void)updateTitle; - (void)updateTitleFor:(IRCTreeItem *)item; - (void)reloadTree; - (void)reloadTreeItem:(IRCTreeItem *)item; - (void)reloadTreeGroup:(IRCTreeItem *)item; - (void)adjustSelection; - (void)maybeToggleFullscreenAfterLaunch; - (void)updateAlphaValueToReflectPreferences; - (void)updateChannelViewBoxContentViewSelection; - (void)updateChannelViewArrangement; - (void)redirectKeyDown:(NSEvent *)e; - (void)inputText:(id)string asCommand:(IRCRemoteCommand)command; - (void)selectItemInSelectedItems:(IRCTreeItem *)selectedItem; - (void)selectItemInSelectedItems:(IRCTreeItem *)selectedItem refreshChannelView:(BOOL)refreshChannelView; - (void)shiftSelection:(nullable IRCTreeItem *)oldItem toItem:(nullable IRCTreeItem *)newItem options:(TVCMainWindowShiftSelectionFlags)selectionOptions; - (void)channelViewSelectionChangeTo:(IRCTreeItem *)selectedItem; - (void)updateDrawingForUserInUserList:(IRCUser *)user; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowSegmentedControlPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowSegmentedController : NSSegmentedControl - (IBAction)segmentedCellClicked:(id)sender; - (void)updateSegmentedController; - (void)updateSegmentedControllerOrigin; @end @interface TVCMainWindowSegmentedControllerCell : NSSegmentedCell @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowSplitViewPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowSplitView.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowSplitView () - (void)restorePositions; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowTextViewAppearancePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowTextViewAppearance.h" NS_ASSUME_NONNULL_BEGIN @class TVCMainWindow; @interface TVCMainWindowTextViewAppearance () - (nullable instancetype)initWithWindow:(TVCMainWindow *)mainWindow; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowTextViewPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowAppearance.h" #import "TVCMainWindowTextView.h" NS_ASSUME_NONNULL_BEGIN @class TVCMainWindowSegmentedController, TVCMainWindowSegmentedControllerCell; @interface TVCMainWindowTextView () - (TVCMainWindowSegmentedController *)segmentedController; - (TVCMainWindowSegmentedControllerCell *)segmentedControllerCell; - (void)updateTextDirection; - (void)updateTextBasedOnPreferredFontSize; - (void)updateSegmentedController; - (void)reloadOriginPoints; - (void)reloadOriginPointsAndRecalculateSize; - (void)recalculateTextViewSize; - (void)recalculateTextViewSizeForced; - (void)resetSpellingIgnores; @end @interface TVCMainWindowTextViewBackground : NSView @end @interface TVCMainWindowTextViewContentView : NSView @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMainWindowTitlebarAccessoryViewPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowTitlebarAccessoryView : NSView @end @interface TVCMainWindowTitlebarAccessoryViewController : NSTitlebarAccessoryViewController @end @interface TVCMainWindowTitlebarAccessoryViewLockButton : NSButton - (void)disableDrawingCustomBackgroundColor; - (void)enableDrawingCustomBackgroundColor; - (void)positionImageOverContent; - (void)positionImageOnLeftSide; - (void)setIconAsLocked; - (void)setIconAsUnlocked; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMemberListAppearancePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMemberListAppearance.h" NS_ASSUME_NONNULL_BEGIN @class TVCMemberList, TVCMainWindow; @interface TVCMemberListAppearance () - (nullable instancetype)initWithMemberList:(TVCMemberList *)MemberList inWindow:(TVCMainWindow *)mainWindow; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMemberListCellPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCMemberList; @interface TVCMemberListRowCell : NSTableRowView - (instancetype)initWithMemberList:(TVCMemberList *)memberList; @end @interface TVCMemberListCell : NSTableCellView - (void)drawWithExpansionFrame; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMemberListPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowAppearance.h" #import "TVCMemberList.h" NS_ASSUME_NONNULL_BEGIN @class TVCMemberListUserInfoPopover; @class IRCChannel; @class IRCChannelMemberListController; @interface TVCMemberList () @property (nonatomic, weak) id keyDelegate; - (TVCMemberListAppearance *)userInterfaceObjects; - (nullable NSVisualEffectView *)visualEffectView; - (TVCMemberListUserInfoPopover *)memberListUserInfoPopover; - (IRCChannelMemberListController *)contentController; - (void)refreshAllDrawings; - (void)refreshDrawingForChangesToPreference:(NSString *)preferenceKey; - (void)assignToChannel:(nullable IRCChannel *)channel; @end @protocol TVCMemberListDelegate @required - (void)memberListKeyDown:(NSEvent *)e; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCMemberListUserInfoPopoverPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCMemberListUserInfoPopover : NSPopover @property (readonly, weak) NSTextField *nicknameField; @property (readonly, weak) NSTextField *usernameField; @property (readonly, weak) NSTextField *addressField; @property (readonly, weak) NSTextField *realNameField; @property (readonly, weak) NSTextField *privilegesField; @property (readonly, weak) NSTextField *awayStatusField; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCNotificationConfigurationViewControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCNotificationConfigurationViewController : NSObject @property (nonatomic, copy) NSArray *notifications; // __kindof TVCNotificationConfiguration* or NSString* @property (nonatomic, assign) BOOL allowsMixedState; // Default: NO - (void)attachToView:(NSView *)view; - (void)reload; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCServerListAppearancePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCServerListAppearance.h" NS_ASSUME_NONNULL_BEGIN @class TVCServerList, TVCMainWindow; @interface TVCServerListAppearance () - (nullable instancetype)initWithServerList:(TVCServerList *)serverList inWindow:(TVCMainWindow *)mainWindow; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCServerListCellPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCServerList; @interface TVCServerListRowCell : NSTableRowView - (instancetype)initWithServerList:(TVCServerList *)serverList; @end @interface TVCServerListGroupRowCell : TVCServerListRowCell @end @interface TVCServerListChildRowCell : TVCServerListRowCell @end @interface TVCServerListCell : NSTableCellView - (void)populateMessageCountBadge; @end @interface TVCServerListCellChildItem : TVCServerListCell @end @interface TVCServerListCellGroupItem : TVCServerListCell @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCServerListPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowAppearance.h" #import "TVCServerList.h" NS_ASSUME_NONNULL_BEGIN @interface TVCServerList () @property (nonatomic, weak) id keyDelegate; - (TVCServerListAppearance *)userInterfaceObjects; - (nullable NSVisualEffectView *)visualEffectView; - (BOOL)leftMouseIsDownInView; - (void)addItemToList:(NSUInteger)rowIndex inParent:(nullable id)parent; - (void)removeItemFromList:(id)object; - (void)refreshAllDrawings; - (void)refreshAllUnreadMessageCountBadges; - (void)refreshMessageCountForItem:(IRCTreeItem *)cellItem; - (void)refreshMessageCountForRow:(NSInteger)rowIndex; @end @protocol TVCServerListDelegate @required - (void)serverListKeyDown:(NSEvent *)e; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCTextFormatterMenuPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #define TVCTextViewIRCFormattingMenuFormatterMenuTag 53037 @interface TVCTextViewIRCFormattingMenu : NSObject @property (readonly, weak) NSMenuItem *formatterMenu; @property (readonly, weak) NSMenu *foregroundColorMenu; @property (readonly, weak) NSMenu *backgroundColorMenu; @property (readonly) BOOL firstResponderSupportsFormatting; @property (readonly) BOOL textIsBold; @property (readonly) BOOL textIsItalicized; @property (readonly) BOOL textIsMonospace; @property (readonly) BOOL textIsStruckthrough; @property (readonly) BOOL textIsUnderlined; @property (readonly) BOOL textHasForegroundColor; @property (readonly) BOOL textHasBackgroundColor; @property (readonly) BOOL textHasSpoiler; - (IBAction)insertBoldCharIntoTextBox:(id)sender; - (IBAction)insertItalicCharIntoTextBox:(id)sender; - (IBAction)insertMonospaceCharIntoTextBox:(id)sender; - (IBAction)insertStrikethroughCharIntoTextBox:(id)sender; - (IBAction)insertUnderlineCharIntoTextBox:(id)sender; - (IBAction)insertForegroundColorCharIntoTextBox:(id)sender; - (IBAction)insertBackgroundColorCharIntoTextBox:(id)sender; - (IBAction)insertSpoilerCharIntoTextBox:(id)sender; - (IBAction)removeBoldCharFromTextBox:(id)sender; - (IBAction)removeItalicCharFromTextBox:(id)sender; - (IBAction)removeMonospaceCharFromTextBox:(id)sender; - (IBAction)removeStrikethroughCharFromTextBox:(id)sender; - (IBAction)removeUnderlineCharFromTextBox:(id)sender; - (IBAction)removeForegroundColorCharFromTextBox:(id)sender; - (IBAction)removeBackgroundColorCharFromTextBox:(id)sender; - (IBAction)removeSpoilerCharFromTextBox:(id)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCTextViewWithIRCFormatterPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCTextViewWithIRCFormatter.h" NS_ASSUME_NONNULL_BEGIN @interface TVCTextViewWithIRCFormatter () @property (nonatomic, copy) NSFont *preferredFont; @property (nonatomic, copy) NSColor *preferredFontColor; - (void)keyDownToSuper:(NSEvent *)e; - (void)updateAllFontSizesToMatchTheDefaultFont; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TVCWK1AutoScrollerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN NS_ASSUME_NONNULL_BEGIN @interface TVCWK1AutoScroller : NSObject @property (readonly) BOOL canScroll; @property (readonly) BOOL viewingBottom; @property (nonatomic, assign) BOOL automaticScrollingEnabled; - (instancetype)initWitFrameView:(WebFrameView *)frameView; - (void)redrawFrame; /* -resetScrollerPosition does not actually move the scroller to the bottom if passed a true value. It simply resets the internal state so that when -restoreScrollerPosition is called, it will automatically scroll to the bottom or not. */ - (void)resetScrollerPosition; - (void)resetScrollerPositionTo:(BOOL)scrolledToBottom; - (void)saveScrollerPosition; - (void)restoreScrollerPosition; @end TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TXAppearancePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXAppearance.h" NS_ASSUME_NONNULL_BEGIN @interface TXAppearance () - (void)updateAppearance; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TXApplicationPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TXApplication : NSApplication + (BOOL)checkForOtherCopiesOfTextualRunning; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TXGlobalModelsPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXGlobalModels.h" NS_ASSUME_NONNULL_BEGIN TEXTUAL_EXTERN NSDateFormatter *TXSharedISOStandardDateFormatter(void); NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TXMasterControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" NS_ASSUME_NONNULL_BEGIN @interface TXMasterController () @property (nonatomic, assign) NSUInteger terminatingClientCount; - (void)applicationWakeStepOne; - (void)applicationWakeStepTwo; - (void)prepareThirdPartyServiceSparkleFramework; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TXMenuControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCServerPropertiesSheetPrivate.h" #import "TXMenuController.h" NS_ASSUME_NONNULL_BEGIN @class IRCTreeItem; @interface TXMenuController () @property (nonatomic, copy, nullable) NSString *pointedNickname; // Takes priority if sender of an action returns nil userInfo value - (void)populateNavigationChannelList; - (IBAction)performNavigationAction:(id)sender; - (IBAction)openHelpMenuItem:(id)sender; - (IBAction)joinChannelClicked:(id)sender; - (void)memberChangeColor:(NSString *)nickname; - (void)memberInChannelViewDoubleClicked:(id)sender; - (void)memberInMemberListDoubleClicked:(id)sender; - (void)memberSendDroppedFiles:(NSArray *)files to:(NSString *)nickname; - (void)memberSendDroppedFiles:(NSArray *)files row:(NSUInteger)row; - (void)memberSendDroppedFilesToSelectedChannel:(NSArray *)files; // Only works if -selectedChannel is a private message - (void)showServerPropertiesSheetForClient:(IRCClient *)client withSelection:(TDCServerPropertiesSheetSelection)selection context:(nullable id)context; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 - (void)manageLicense:(id)sender activateLicenseKeyWithURL:(NSURL *)licenseKeyURL; - (void)manageLicense:(id)sender activateLicenseKey:(nullable NSString *)licenseKey; - (void)manageLicense:(id)sender activateLicenseKey:(nullable NSString *)licenseKey licenseKeyPassedByArgument:(BOOL)licenseKeyPassedByArgument; #endif - (void)toggleMuteOnNotificationsShortcutOn:(BOOL)toggleOn; - (void)toggleMuteOnNotificationSoundsShortcutOn:(BOOL)toggleOn; - (void)navigateToTreeItemAtURL:(NSURL *)url; - (void)navigateToTreeItemWithIdentifier:(NSString *)identifier; - (void)navigateToTreeItem:(IRCTreeItem *)item; - (IBAction)emptyAction:(id)sender TEXTUAL_DEPRECATED("Do not target this method"); @end @interface TXMenuControllerMainWindowProxy : NSObject - (IBAction)showWelcomeSheet:(id)sender; - (IBAction)manageLicense:(id)sender; - (IBAction)openStandaloneStoreWebpage:(id)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TXSharedApplicationPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXSharedApplication.h" NS_ASSUME_NONNULL_BEGIN #define windowController() [TXSharedApplication sharedWindowController] #define sharedNotificationController() [TXSharedApplication sharedNotificationController] #define sharedPluginManager() [TXSharedApplication sharedPluginManager] @class OELReachability; @class THOPluginManager; @class TDCFileTransferDialog; @class TLONotificationController, TLOSpeechSynthesizer; @class TVCLogControllerPrintingOperationQueue; @class TXWindowController; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 @class TLOEncryptionManager; #endif #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @class TDCLicenseManagerDialog; #endif @interface TXSharedApplication () #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 + (TLOEncryptionManager *)sharedEncryptionManager; #endif + (OELReachability *)sharedNetworkReachabilityNotifier; + (TLONotificationController *)sharedNotificationController; + (THOPluginManager *)sharedPluginManager; + (TVCLogControllerPrintingOperationQueue *)sharedPrintingQueue; + (TLOSpeechSynthesizer *)sharedSpeechSynthesizer; + (TXWindowController *)sharedWindowController; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 + (TDCLicenseManagerDialog *)sharedLicenseManagerDialog; #endif + (TDCFileTransferDialog *)sharedFileTransferDialog; @end @interface NSObject (TXSharedApplicationObjectExtensionPrivate) + (void)setGlobalMasterControllerClassReference:(TXMasterController *)masterController; @end TEXTUAL_EXTERN os_log_t ApplicationTerminationLogSubsystem(void); #define LogToConsoleTerminationProgress(_message, ...) \ LogToConsoleDebugWithSubsystem(ApplicationTerminationLogSubsystem(), _message, ##__VA_ARGS__) \ NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TXWindowControllerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TXWindowController : NSObject /* By default only one instance of a particular class (window) may be maintained a reference to at any given time. Supply a related object in order to uniquely identify a window. */ - (void)addWindowToWindowList:(id)window; - (void)addWindowToWindowList:(id)window inRelationTo:(nullable id)relatedObject; - (void)addWindowToWindowList:(id)window withDescription:(NSString *)windowDescription; - (void)removeWindowFromWindowList:(id)window; - (void)removeWindowFromWindowList:(id)window inRelationTo:(nullable id)relatedObject; - (nullable id)windowFromWindowList:(NSString *)windowDescription; - (NSArray *)windowsFromWindowList:(NSArray *)windowDescriptions; - (BOOL)maybeBringWindowForward:(NSString *)windowDescription; - (void)popMainWindowSheetIfExists; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Private/TextualPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import #import #import #import #import "StaticDefinitions.h" /* Defines for script support instead of importing the entire Carbon framework for three items. */ #ifndef kASAppleScriptSuite #define kASAppleScriptSuite 'ascr' #endif #ifndef kASSubroutineEvent #define kASSubroutineEvent 'psbr' #endif #ifndef keyASSubroutineName #define keyASSubroutineName 'snam' #endif #import "TXSharedApplicationPrivate.h" ================================================ FILE: Sources/App/Classes/Headers/Private/WKWebViewPrivate.h ================================================ typedef NS_OPTIONS(NSUInteger, _WKFindOptions) { _WKFindOptionsCaseInsensitive = 1 << 0, _WKFindOptionsAtWordStarts = 1 << 1, _WKFindOptionsTreatMedialCapitalAsWordStart = 1 << 2, _WKFindOptionsBackwards = 1 << 3, _WKFindOptionsWrapAround = 1 << 4, _WKFindOptionsShowOverlay = 1 << 5, _WKFindOptionsShowFindIndicator = 1 << 6, _WKFindOptionsShowHighlight = 1 << 7, _WKFindOptionsDetermineMatchIndex = 1 << 8 }; typedef const struct OpaqueWKPage *WKPageRef; typedef const struct OpaqueWKInspector *WKInspectorRef; WKInspectorRef WKPageGetInspector(WKPageRef page); void WKInspectorShow(WKInspectorRef inspectorRef); @interface WKWebView () - (WKPageRef)_pageForTesting; @end @interface WKView : NSView - (WKPageRef)pageRef; @end @interface _WKProcessPoolConfiguration : NSObject @property (nonatomic) NSUInteger maximumProcessCount; @end @interface WKProcessPool () - (instancetype)_initWithConfiguration:(_WKProcessPoolConfiguration *)configuration; - (_WKProcessPoolConfiguration *)_configuration; @end @interface NSView (WKViewSwizzle) @end @interface WKPreferences () @property (nonatomic, setter=_setAllowFileAccessFromFileURLs:) BOOL _allowFileAccessFromFileURLs; // 10.11 @property (nonatomic, setter=_setDeveloperExtrasEnabled:) BOOL _developerExtrasEnabled; // 10.11 @end @interface WKWebViewConfiguration () @property (nonatomic, setter=_setAllowUniversalAccessFromFileURLs:) BOOL _allowUniversalAccessFromFileURLs; // 10.12 @end /* Defined in WebKit/Source/WebKit/UIProcess/API/Cocoa/WKNavigationDelegatePrivate.h */ /* Breaking the law, breaking the law... */ typedef NS_ENUM(NSInteger, _WKProcessTerminationReason) { _WKProcessTerminationReasonExceededMemoryLimit, _WKProcessTerminationReasonExceededCPULimit, _WKProcessTerminationReasonRequestedByClient, _WKProcessTerminationReasonCrash, }; ================================================ FILE: Sources/App/Classes/Headers/Private/WebScriptObjectHelperPrivate.h ================================================ /* ********************************************************************* Copyright (c) 2010 - 2018 Codeux Software, LLC Please see ACKNOWLEDGEMENT for additional information. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of "Codeux Software, LLC", nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *********************************************************************** */ TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN #import NS_ASSUME_NONNULL_BEGIN @interface WebScriptObject (TXWebScriptObjectHelper) - (nullable id)toCommonInContext:(JSContextRef)jsContextRef; @end TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TDCAlert.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCAlert.h" NS_ASSUME_NONNULL_BEGIN TEXTUAL_EXTERN NSString * const TDCAlertSuppressionPrefix; typedef NS_ENUM(NSUInteger, TDCAlertResponse) { TDCAlertResponseDefault = 1000, TDCAlertResponseAlternate = 1001, TDCAlertResponseOther = 1002 }; typedef void (^TDCAlertCompletionBlock)(TDCAlertResponse buttonClicked, BOOL suppressed, id _Nullable underlyingAlert); @interface TDCAlert : NSObject /* Return the actual suppression key used internally. Do not feed this to the suppressionKey: field of these alerts. This is what is fed to that field turns into once the alert is processed. */ + (NSString *)suppressionKeyWithBase:(NSString *)base; /* All methods consider it valid to have suppression text but no suppression key. When suppression text is present, the user will be presented with a checkbox titled the value of suppressionText. If suppressionKey is not set, then nothing is recorded to user defaults. This provides a convenient way to ask the user a an unrelated question and receive a response. The response is in the callback block. If suppressed is YES, then the user enabled the checkbox. Even if no suppression key is present. */ #pragma mark - #pragma mark Modal Alerts (Panel) + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate; + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText; + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView; + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText suppressionResponse:(nullable BOOL *)suppressionResponse; + (BOOL)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView suppressionResponse:(nullable BOOL *)suppressionResponse; + (TDCAlertResponse)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther; + (TDCAlertResponse)modalAlertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView suppressionResponse:(nullable BOOL *)suppressionResponse; #pragma mark - #pragma mark Non-blocking Alerts (Panel) /* The underlying TVCAlert object returned by the non-blocking methods does NOT need to be retained. Simply ignore the return value if you have no use in accessing the object. */ /* Return value is only nil if completion block is immediately returned for suppression. */ + (TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate; + (TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther; + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText; + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; + (nullable TVCAlert *)alertWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; #pragma mark - #pragma mark Non-blocking Alerts (Sheet) + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther; + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther accessoryView:(nullable NSView *)accessoryView completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; + (void)alertSheetWithWindow:(NSWindow *)window body:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate otherButton:(nullable NSString *)buttonOther suppressionKey:(nullable NSString *)suppressKey suppressionText:(nullable NSString *)suppressText accessoryView:(nullable NSView *)accessoryView completionBlock:(nullable TDCAlertCompletionBlock)completionBlock; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TDCInputPrompt.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCAlert.h" NS_ASSUME_NONNULL_BEGIN @interface TDCInputPrompt : TDCAlert + (TVCAlertResponseButton)promptWithMessage:(NSString *)bodyText title:(NSString *)titleText defaultButton:(NSString *)buttonDefault alternateButton:(nullable NSString *)buttonAlternate prefillString:(nullable NSString *)prefillString resultString:(NSString * _Nonnull * _Nonnull )resultString; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TDCSheetBase.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TVCValidatedComboBox, TVCValidatedTextField; @interface TDCSheetBase : NSObject @property (nonatomic, weak) id delegate; @property (nonatomic, weak) NSWindow *window; @property (nonatomic, strong) IBOutlet NSWindow *sheet; // Window being attached @property (nonatomic, weak) IBOutlet NSButton *okButton; @property (nonatomic, weak) IBOutlet NSButton *cancelButton; - (instancetype)init NS_UNAVAILABLE; /* Window is allowed to be set after init */ - (instancetype)initWithWindow:(nullable NSWindow *)window; // NS_DESIGNATED_INITIALIZER - (void)startSheet; - (void)endSheet; - (IBAction)ok:(id)sender; - (IBAction)cancel:(id)sender; - (void)close; - (BOOL)okOrErrorForTextField:(TVCValidatedTextField *)textField; - (BOOL)okOrErrorForComboBox:(TVCValidatedComboBox *)comboBox; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TDCWindowBase.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TDCWindowBase : NSObject @property (nonatomic, weak) id delegate; @property (nonatomic, strong) IBOutlet NSWindow *window; @property (nonatomic, weak) IBOutlet NSButton *okButton; @property (nonatomic, weak) IBOutlet NSButton *cancelButton; - (void)show; - (void)close; - (IBAction)ok:(id)sender; - (IBAction)cancel:(id)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/THOPluginManager.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2024 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import /** * Subsystem that plugin can use for logging using `os_log` and related systems. * * Plugins are loaded directly into Textual and are not executed on a separate * process. As such, the returned subsystem uses the same identifier of Textual. * The category is different. The category is in the format: * `Extension['']` * * Accessing the logging subsystem through this function is not optimized * if aggressive logging is performed. In that case, retain a reference, * or create a subsystem outside of this initializer. * * @return Subsystem that plugin can use for logging. */ #define THOPluginLoggingSubsystem() \ _THOPluginLoggingSubsystemForBundle([NSBundle bundleForClass:[self class]]) TEXTUAL_EXTERN os_log_t _THOPluginLoggingSubsystemForBundle(NSBundle *bundle); ================================================ FILE: Sources/App/Classes/Headers/THOPluginProtocol.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "THOPluginManager.h" #import "IRCCommandIndex.h" #import "TVCLogLine.h" NS_ASSUME_NONNULL_BEGIN @class AHHyperlinkScannerResult; @class IRCClient, IRCChannel, IRCChannelUser, IRCPrefix, IRCMessage; @class TVCLogController; @class THOPluginDidPostNewMessageConcreteObject; @class THOPluginDidReceiveServerInputConcreteObject; @class THOPluginOutputSuppressionRule; @class THOPluginWebViewJavaScriptPayloadConcreteObject; #pragma mark - #pragma mark Localization #define TPILocalizedString(k, ...) TXLocalizedStringAlternative(TPIBundleFromClass(), k, ##__VA_ARGS__) /** * @brief Returns the NSBundle that owns the calling class. */ #define TPIBundleFromClass() [NSBundle bundleForClass:[self class]] /** * A plugin must declare the minimum version of Textual that it is compatible with. * * Textual declares the constant named THOPluginProtocolCompatibilityMinimumVersion. * This constant is compared against the minimum version that a plugin specifies. * If the plugin's value is equal to or greater than this constant, then the plugin * is considered safe to load. * * Unlike the version information that visible to the end user, this constant does * not change often. It only changes when modifications have been made to Textual’s * codebase that may result in crashes when loading existing plugins. * * For example, even though Textual’s visible version number is “5.0.4”, the value * of this constant is “5.0.0” * * To declare compatibility, add a new entry to a plugin's Info.plist file with * the key named: "MinimumTextualVersion" - Set the value of this entry, as a * String, to the return value of THOPluginProtocolCompatibilityMinimumVersion. * * @return "7.2.4" as of June 30, 2024 */ extern NSString * const THOPluginProtocolCompatibilityMinimumVersion; /** * The `THOPluginProtocol` protocol defines methods and properties that the * primary class of a plugin can inherit from. */ @protocol THOPluginProtocol @optional #pragma mark - #pragma mark Initialization /** @name Initialization */ /** * @brief Method invoked during initialization of a plugin. * * @discussion This method is invoked very early on. It occurs once the principal * class of the plugin has been allocated and is guaranteed to be the first call * home that a plugin will receive. */ - (void)pluginLoadedIntoMemory; /** * @brief Method invoked prior to deallocation of a plugin. */ - (void)pluginWillBeUnloadedFromMemory; #pragma mark - #pragma mark Input Manipulation /** @name Input Manipulation */ /** * @brief Method invoked to inform the plugin that a plain text message was received * (*PRIVMSG*, *ACTION*, or *NOTICE*) * * @discussion This method is invoked on the main thread which means that slow code * can lockup the user interface of Textual. If you have no intent to ignore content, * then do work in the background and immediately return `YES`. * * @param text The message contents * @param textAuthor The author (sender) of the message * @param textDestination The channel that the message is destined for * @param lineType The line type of the message * * Possible values: `TVCLogLineTypePrivateMessage`, `TVCLogLineTypeAction`, * `TVCLogLineTypeNotice` * @param client The client the message was received on * @param receivedAt The date & time of the message. Depending on whether a custom * value was specified using the server-time IRCv3 capability, this `NSDate` * object may be very far in the past, or even possibly in the future. * @param wasEncrypted Whether or not the message was encrypted * * @return `YES` to display the contents of the message to the user, `NO` otherwise. */ - (BOOL)receivedText:(NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination asLineType:(TVCLogLineType)lineType onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt wasEncrypted:(BOOL)wasEncrypted; /** * @brief Method used to modify and/or completely ignore incoming data from the server. * * @warning This method is invoked on each plugin in the order loaded. This method * does not stop for the first result returned which means that value being passed may * have been modified by a plugin above the one being talked to. * * @warning Textual does not perform validation against the instance of `IRCMessage` that * is returned which means that if Textual tries to access specific information which * has been improperly modified or removed, then the entire application may crash. * * @param input An instance of `IRCMessage` * @param client The client responsible for the event * * @return The original and/or modified copy of `IRCMessage` or `nil` to prevent the data * from being processed altogether. */ - (nullable IRCMessage *)interceptServerInput:(IRCMessage *)input for:(IRCClient *)client; /** * @brief Method used to modify and/or completely ignore text entered into the main text * field of Textual. * * @warning This method is invoked on each plugin in the order loaded. This method * does not stop for the first result returned which means that value being passed may * have been modified by a plugin above the one being talked to. * * @param input The value of the text field as either an instance of `NSString` or * `NSAttributedString`. * @param command Textual allows the end user to send text entered into the text field as * an action without using the `/me` command. When this occurs, Textual informs lower-level * APIs of this intent by changing the value of this parameter from “privmsg” to “action” — * In most cases a plugin should disregard this parameter and pass it untouched. * * @return The original and/or modified copy of input or `nil` to prevent the data from * being processed altogether. */ - (nullable id)interceptUserInput:(id)input command:(IRCRemoteCommand)command; #pragma mark - #pragma mark Preferences Pane /** @name Preferences */ /** * @brief Defines an `NSView` used by the Preferences window of Textual to * allow user-interactive configuration of the plugin. * * @return An instance of NSView with a width of at least 590. */ @property (nonatomic, readonly, strong) NSView *pluginPreferencesPaneView; /** * @brief Defines an `NSString` which is used by the Preferences window of * Textual to create a new entry in its navigation list. */ @property (nonatomic, readonly, copy) NSString *pluginPreferencesPaneMenuItemName; #pragma mark - #pragma mark Renderer Events /** @name Renderer Events */ /** * @brief Method invoked prior to a message being converted to its HTML equivalent. * * @discussion This methods can be used to modify the text that will be displayed for a * certain message by replacing one or more segments of it. * * Considerations: * * 1. `nil` or a string with zero length indicates that there is no interest in modifying * `newMessage` * 2. There is no way to inform the renderer that you do not want a specific value of * `newMessage` shown to the end user. Use the various other methods provided by the * `THOPluginProtocol` to accomplish that task. * * @warning This method is invoked on each plugin in the order loaded. This method does not * stop for the first result returned which means that value being passed may have been * modified by a plugin above the one being talked to. * * @warning Under no circumstances should you insert HTML at this point. Doing so will result * in undefined behavior. * * @param newMessage An unedited copy of the message being rendered * @param viewController The view responsible for the event * @param lineType The line type of `newMessage` * @param memberType The member type of `newMessage` * * @return The original and/or modified copy of `newMessage` */ - (NSString *)willRenderMessage:(NSString *)newMessage forViewController:(TVCLogController *)viewController lineType:(TVCLogLineType)lineType memberType:(TVCLogLineMemberType)memberType; #pragma mark - #pragma mark Subscribed Events /** @name Subscribed Events */ /** * @brief Defines a list of commands that the plugin will support as user input * from the main text field. * * @discussion Considerations: * * 1. If a command is a number, then insert it into the array as an `NSString` * 2. If a plugin tries to add a command already built into Textual onto * this list, it will not work. * 3. It is possible, but unlikely, that another plugin the end user has * loaded is subscribed to the same command. When that occurs, all plugins * subscribed to the command will be informed of when the command is performed. * 4. To avoid conflicts, a plugin cannot subscribe to a command already * defined by a script. If a script and a plugin both share the same command, * then neither will be executed and an error will be printed to the console. * * @return An `NSArray` containing a lowercase list of commands that the plugin * will support as user input from the main text field. */ @property (nonatomic, readonly, copy) NSArray *subscribedUserInputCommands; /** * @brief Method invoked when a subscribed user input command requires processing. * * @param client The client responsible for the event * @param commandString The name of the command * @param messageString Data that follows `commandString` */ - (void)userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString; /** * @brief Defines a list of commands that the plugin will support as server input. * * @return An `NSArray` containing a lowercase list of commands that the plugin * will support as server input. * * @discussion If a command is a number, then insert it into the array as an `NSString` */ @property (nonatomic, readonly, copy) NSArray *subscribedServerInputCommands; /** * @brief Method invoked when a subscribed server input command requires processing. * * @param inputObject An instance of THOPluginDidReceiveServerInputConcreteObject * @param client The client responsible for the event * * @see THOPluginDidReceiveServerInputConcreteObject */ - (void)didReceiveServerInput:(THOPluginDidReceiveServerInputConcreteObject *)inputObject onClient:(IRCClient *)client; #pragma mark - #pragma mark WebView Events /** @name WebView Events */ /** * @brief Method invoked when the Document Object Model (DOM) of a view has been modified. * * @discussion This method is invoked when a message has been added to the Document Object * Model (DOM) of viewController * * @warning Do not do any heavy work when the * [isProcessedInBulk]([THOPluginDidPostNewMessageConcreteObject isProcessedInBulk]) property * of `messageObject` is set to `YES` because thousand of other messages may be processing at * the same time. * * @warning This method is invoked on an asynchronous background dispatch queue. Not the * main thread. If you interact with WebKit when this method is invoked, then make sure * that you do so on the main thread. If you don't, WebKit will throw an exception. * * @param messageObject An instance of THOPluginDidPostNewMessageConcreteObject * @param viewController The view responsible for the event * * @see THOPluginDidPostNewMessageConcreteObject */ - (void)didPostNewMessage:(THOPluginDidPostNewMessageConcreteObject *)messageObject forViewController:(TVCLogController *)viewController; /** * @brief Method invoked when the JavaScript function `app.sendPluginPayload()` is executed. * * @discussion A plugin that injects JavaScript into Textual's WebView can use this method * to send data back to the plugin. * * A payload can be passed by invoking the JavaScript function * `app.sendPluginPayload(payloadLabel, payloadContent)` * * @warning This method is invoked on an asynchronous background dispatch queue. Not the * main thread. If you interact with WebKit when this method is invoked, then make sure * that you do so on the main thread. If you don't, WebKit will throw an exception. * * @param payloadObject An instance of THOPluginWebViewJavaScriptPayloadConcreteObject * @param viewController The view responsible for the event * * @see THOPluginWebViewJavaScriptPayloadConcreteObject */ - (void)didReceiveJavaScriptPayload:(THOPluginWebViewJavaScriptPayloadConcreteObject *)payloadObject fromViewController:(TVCLogController *)viewController; #pragma mark - #pragma mark Reserved Calls /* The behavior of this method call is undefined. It exists for internal purposes for the plugins packaged with Textual by default. It is not recommended to use it, or try to understand it. */ @property (nonatomic, readonly, copy) NSArray *pluginOutputSuppressionRules; @end #pragma mark - /** * This object is a container for values related to * [THOPluginProtocol didPostNewMessage:forViewController:] */ @interface THOPluginDidPostNewMessageConcreteObject : NSObject /** * @brief Whether the message was posted as a result of a bulk operation */ @property (readonly) BOOL isProcessedInBulk; /** * @brief The contents of the message visible to the end user */ @property (readonly, copy) NSString *messageContents; /** * @brief The ID of the message that can be used to access it using `getElementByID()` */ @property (readonly, copy) NSString *lineNumber; /** * @brief The nickname of the person and/or server responsible for producing the * message. * * @discussion This value may be empty. */ @property (readonly, copy, nullable) NSString *senderNickname; /** * @brief The line type of the message */ @property (readonly) TVCLogLineType lineType; /** * @brief The member type of the message */ @property (readonly) TVCLogLineMemberType memberType; /** * @brief The date & time displayed left of the message in the WebView */ @property (readonly, copy) NSDate *receivedAt; /** * @brief Array of URLs found in the message body */ @property (readonly, copy) NSArray *listOfHyperlinks; /** * @brief List of users from the channel that appear in the message */ @property (readonly, copy) NSSet *listOfUsers; /** * @brief Whether or not a highlight word was matched */ @property (readonly) BOOL keywordMatchFound; @end #pragma mark - /** * This object is a container for values related to * [THOPluginProtocol didReceiveServerInput:onClient:] */ @interface THOPluginDidReceiveServerInputConcreteObject : NSObject /** * @brief Whether the input was from a regular user or from a server */ @property (readonly) BOOL senderIsServer; /** * @brief The nickname section of the sender's hostmask * * @discussion The value of this property is the server address if senderIsServer is `YES` */ @property (readonly, copy) NSString *senderNickname; /** * @brief The username (ident) section of the sender's hostmask */ @property (readonly, copy, nullable) NSString *senderUsername; /** * @brief The address section of the sender's hostmask */ @property (readonly, copy, nullable) NSString *senderAddress; /** * @brief The combined hostmask of the sender */ @property (readonly, copy) NSString *senderHostmask; /** * @brief The date & time during which the input was received * * @discussion If the original message specifies a custom value using the server-time * capability, then the value of this property will reflect the value defined by the * server-time capability; not the exact date & time it was received on the socket. */ @property (readonly, copy) NSDate *receivedAt; /** * @brief The input itself */ @property (readonly, copy) NSString *messageSequence; /** * @brief The input, split up into sections */ @property (readonly, copy) NSArray *messageParameters; /** * @brief The input's command */ @property (readonly, copy) NSString *messageCommand; /** * @brief The value of -messageCommand as an integer */ @property (readonly) NSUInteger messageCommandNumeric; /** * @brief The server address of the IRC network * * @discussion The value of this attribute is the address of the server that * Textual is currently connected to and may differ from senderNickname even * if senderIsServer is `YES` */ @property (readonly, copy, nullable) NSString *networkAddress; /** * @brief The name of the IRC network */ @property (readonly, copy, nullable) NSString *networkName; @end #pragma mark - /** * This object is a container for values related to * [THOPluginProtocol didReceiveJavaScriptPayload:fromViewController:] */ @interface THOPluginWebViewJavaScriptPayloadConcreteObject : NSObject /** * @brief A description of the payload */ @property (readonly, copy) NSString *payloadLabel; /** * @brief The payload contents */ @property (readonly, copy, nullable) id payloadContents; @end #pragma mark - @interface THOPluginOutputSuppressionRule : NSObject @property (nonatomic, copy) NSString *match; @property (nonatomic, assign) BOOL restrictConsole; @property (nonatomic, assign) BOOL restrictChannel; @property (nonatomic, assign) BOOL restrictPrivateMessage; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/THOUnicodeHelper.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface THOUnicodeHelper : NSObject + (BOOL)isPrivate:(UniChar)c; + (BOOL)isIdeographic:(UniChar)c; + (BOOL)isIdeographicOrPrivate:(UniChar)c; + (BOOL)isAlphabeticalCodePoint:(NSInteger)c; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TLOEncryptionManager.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ typedef void (^TLOEncryptionManagerEncodingDecodingCallbackBlock)(NSString *originalString, BOOL wasEncrypted); typedef void (^TLOEncryptionManagerInjectCallbackBlock)(NSString *encodedString); ================================================ FILE: Sources/App/Classes/Headers/TLOInternetAddressLookup.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @protocol TLOInternetAddressLookupDelegate; @interface TLOInternetAddressLookup : NSObject @property (nonatomic, assign) BOOL IPv4AddressIsValid; // Defaults to YES @property (nonatomic, assign) BOOL IPv6AddressIsValid; // ^ - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithDelegate:(id )delegate NS_DESIGNATED_INITIALIZER; - (void)performLookup; - (void)cancelLookup; @end @protocol TLOInternetAddressLookupDelegate @required - (void)internetAddressLookupReturnedAddress:(NSString *)address; - (void)internetAddressLookupFailed; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TLOKeyEventHandler.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #define TXKeyReturnCode 0x24 #define TXKeyTabCode 0x30 #define TXKeySpacebarCode 0x31 #define TXKeyBackspaceCode 0x33 #define TXKeyEscapeCode 0x35 #define TXKeyEnterCode 0x4C #define TXKeyHomeCode 0x73 #define TXKeyPageUpCode 0x74 #define TXKeyDeleteCode 0x75 #define TXKeyEndCode 0x77 #define TXKeyPageDownCode 0x79 #define TXKeyLeftArrowCode 0x7B #define TXKeyRightArrowCode 0x7C #define TXKeyDownArrowCode 0x7D #define TXKeyUpArrowCode 0x7E @protocol TLOKeyEventHandlerPrototype @optional - (void)setKeyHandlerTarget:(id)target; - (void)registerSelector:(SEL)selector key:(NSUInteger)keyCode modifiers:(NSUInteger)modifiers; - (void)registerSelector:(SEL)selector character:(UniChar)character modifiers:(NSUInteger)modifiers; - (void)registerSelector:(SEL)selector characters:(NSRange)characterRange modifiers:(NSUInteger)modifiers; @end @interface TLOKeyEventHandler : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithTarget:(id)target NS_DESIGNATED_INITIALIZER; - (BOOL)processKeyEvent:(NSEvent *)event; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TLOLinkParser.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class AHHyperlinkScannerResult; @interface TLOLinkParser : NSObject + (NSArray *)locateLinksInString:(NSString *)string; @property (readonly, class, copy) NSArray *bannedLineTypes; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TLONotificationController.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import NS_ASSUME_NONNULL_BEGIN @class IRCChannel; typedef NS_ENUM(NSUInteger, TXNotificationType) { TXNotificationTypeHighlight = 1000, TXNotificationTypeNewPrivateMessage = 1001, TXNotificationTypeChannelMessage = 1002, TXNotificationTypeChannelNotice = 1003, TXNotificationTypePrivateMessage = 1004, TXNotificationTypePrivateNotice = 1005, TXNotificationTypeKick = 1006, TXNotificationTypeInvite = 1007, TXNotificationTypeConnect = 1008, TXNotificationTypeDisconnect = 1009, TXNotificationTypeAddressBookMatch = 1010, TXNotificationTypeFileTransferSendSuccessful = 1011, TXNotificationTypeFileTransferReceiveSuccessful = 1012, TXNotificationTypeFileTransferSendFailed = 1013, TXNotificationTypeFileTransferReceiveFailed = 1014, TXNotificationTypeFileTransferReceiveRequested = 1015, TXNotificationTypeUserJoined = 1016, TXNotificationTypeUserParted = 1017, TXNotificationTypeUserDisconnected = 1018 }; @interface TLONotificationController : NSObject @property (nonatomic, assign) BOOL areNotificationsDisabled; - (NSString *)titleForEvent:(TXNotificationType)event; @end @interface TLONotificationController (Preferences) - (nullable NSString *)soundForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel; - (BOOL)speakEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel; - (BOOL)notificationEnabledForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel; - (BOOL)disabledWhileAwayForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel; - (BOOL)bounceDockIconForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel; - (BOOL)bounceDockIconRepeatedlyForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TLOSoundPlayer.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TLOSoundPlayer : NSObject + (void)playAlertSound:(NSString *)name; + (NSArray *)uniqueListOfSounds; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TLOpenLink.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TLOpenLink : NSObject + (void)open:(NSURL *)url; + (void)open:(NSURL *)url inBackground:(BOOL)inBackground; + (void)openWithString:(NSString *)url; + (void)openWithString:(NSString *)url inBackground:(BOOL)inBackground; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCApplicationInfo.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TPCApplicationInfo : NSObject + (BOOL)sandboxEnabled; + (nullable NSDate *)applicationLaunchDate; + (NSTimeInterval)timeIntervalSinceApplicationLaunch; + (NSTimeInterval)timeIntervalSinceApplicationInstall; + (NSUInteger)applicationRunCount; + (NSString *)applicationName; + (NSString *)applicationNameWithoutVersion; + (NSString *)applicationVersion; + (NSString *)applicationVersionShort; + (NSString *)applicationBundleIdentifier; + (NSString *)applicationBuildScheme; + (int)applicationProcessID; + (NSDictionary *)applicationInfoPlist; + (NSTimeInterval)applicationBirthday; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCPathInfo.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TPCPathInfo : NSObject #pragma mark - #pragma mark Application Specific @property (readonly, class, copy) NSString *applicationBundle; @property (readonly, class, copy) NSURL *applicationBundleURL; @property (readonly, class, copy) NSString *applicationResources; @property (readonly, class, copy) NSURL *applicationResourcesURL; @property (readonly, class, copy, nullable) NSString *applicationCaches; @property (readonly, class, copy, nullable) NSURL *applicationCachesURL; @property (readonly, class, copy, nullable) NSString *groupContainer; @property (readonly, class, copy, nullable) NSURL *groupContainerURL; @property (readonly, class, copy, nullable) NSString *groupContainerApplicationCaches; @property (readonly, class, copy, nullable) NSURL *groupContainerApplicationCachesURL; @property (readonly, class, copy, nullable) NSString *applicationSupport; @property (readonly, class, copy, nullable) NSURL *applicationSupportURL; @property (readonly, class, copy, nullable) NSString *groupContainerApplicationSupport; @property (readonly, class, copy, nullable) NSURL *groupContainerApplicationSupportURL; @property (readonly, class, copy, nullable) NSString *applicationLogs; @property (readonly, class, copy, nullable) NSURL *applicationLogsURL; @property (readonly, class, copy) NSString *applicationTemporary; @property (readonly, class, copy) NSURL *applicationTemporaryURL; @property (readonly, class, copy) NSString *applicationTemporaryProcessSpecific; @property (readonly, class, copy) NSURL *applicationTemporaryProcessSpecificURL; @property (readonly, class, copy) NSString *bundledExtensions; @property (readonly, class, copy) NSURL *bundledExtensionsURL; @property (readonly, class, copy) NSString *bundledScripts; @property (readonly, class, copy) NSURL *bundledScriptsURL; @property (readonly, class, copy) NSString *bundledThemes; @property (readonly, class, copy) NSURL *bundledThemesURL; @property (readonly, class, copy, nullable) NSString *customExtensions; @property (readonly, class, copy, nullable) NSURL *customExtensionsURL; @property (readonly, class, copy, nullable) NSString *customScripts; @property (readonly, class, copy, nullable) NSURL *customScriptsURL; @property (readonly, class, copy, nullable) NSString *customThemes; @property (readonly, class, copy, nullable) NSURL *customThemesURL; #pragma mark - #pragma mark System Specific @property (readonly, class, copy, nullable) NSString *systemApplications; @property (readonly, class, copy, nullable) NSURL *systemApplicationsURL; @property (readonly, class, copy) NSString *systemDiagnosticReports; @property (readonly, class, copy) NSURL *systemDiagnosticReportsURL; #pragma mark - #pragma mark User Specific @property (readonly, class, copy, nullable) NSString *userApplicationScripts; @property (readonly, class, copy, nullable) NSURL *userApplicationScriptsURL; @property (readonly, class, copy) NSString *userDiagnosticReports; @property (readonly, class, copy) NSURL *userDiagnosticReportsURL; @property (readonly, class, copy, nullable) NSString *userDownloads; @property (readonly, class, copy, nullable) NSURL *userDownloadsURL; @property (readonly, class, copy) NSString *userHome; @property (readonly, class, copy) NSURL *userHomeURL; @property (readonly, class, copy, nullable) NSString *userPreferences; @property (readonly, class, copy, nullable) NSURL *userPreferencesURL; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCPreferencesImportExport.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TPCPreferencesImportExport : NSObject + (void)importInWindow:(NSWindow *)window; + (void)exportInWindow:(NSWindow *)window; + (NSDictionary *)exportedPreferencesDictionary; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCPreferencesLocal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLONotificationController.h" #import "TPCPreferences.h" NS_ASSUME_NONNULL_BEGIN TEXTUAL_EXTERN NSString * const TPCPreferencesThemeNameDefaultsKey; TEXTUAL_EXTERN NSString * const TPCPreferencesThemeFontNameDefaultsKey; TEXTUAL_EXTERN NSString * const TPCPreferencesThemeFontSizeDefaultsKey; TEXTUAL_EXTERN NSString * const TPCPreferencesThemeNameMissingLocallyDefaultsKey; TEXTUAL_EXTERN NSString * const TPCPreferencesThemeFontNameMissingLocallyDefaultsKey; TEXTUAL_EXTERN NSUInteger const TPCPreferencesDictionaryVersion; typedef NS_ENUM(NSUInteger, TXNicknameHighlightMatchType) { TXNicknameHighlightMatchTypePartial = 0, TXNicknameHighlightMatchTypeExact, TXNicknameHighlightMatchTypeRegularExpression, }; typedef NS_ENUM(NSUInteger, TXTabKeyAction) { TXTabKeyActionNicknameComplete = 0, TXTabKeyActionUnreadChannel, TXTabKeyActionNone = 100, }; typedef NS_ENUM(NSUInteger, TXUserDoubleClickAction) { TXUserDoubleClickActionWhois = 100, TXUserDoubleClickActionPrivateMessage = 200, TXUserDoubleClickActionInsertTextField = 300, }; typedef NS_ENUM(NSUInteger, TXNoticeSendLocation) { TXNoticeSendLocationServerConsole = 0, TXNoticeSendLocationSelectedChannel = 1, TXNoticeSendLocationQuery = 2, }; typedef NS_ENUM(NSUInteger, TXCommandWKeyAction) { TXCommandWKeyActionCloseWindow = 0, TXCommandWKeyActionPartChannel = 1, TXCommandWKeyActionDisconnect = 2, TXCommandWKeyActionTerminate = 3, }; typedef NS_ENUM(NSUInteger, TXHostmaskBanFormat) { TXHostmaskBanFormatWHNIN = 0, // With Hostmask, No Username/Nickname TXHostmaskBanFormatWHAINN = 1, // With Hostmask and Username, No Nickname TXHostmaskBanFormatWHANNI = 2, // With Hostmask and Nickname, No Username TXHostmaskBanFormatExact = 3, // Exact Match }; typedef NS_ENUM(NSUInteger, TVCMainWindowTextViewFontSize) { TVCMainWindowTextViewFontSizeNormal = 1, TVCMainWindowTextViewFontSizeLarge = 2, TVCMainWindowTextViewFontSizeExtraLarge = 3, TVCMainWindowTextViewFontSizeHumongous = 4, }; typedef NS_ENUM(NSUInteger, TXFileTransferRequestReply) { TXFileTransferRequestReplyIgnore = 1, TXFileTransferRequestReplyOpenDialog = 2, TXFileTransferRequestReplyAutomaticallyDownload = 3, }; typedef NS_ENUM(NSUInteger, TXFileTransferIPAddressMethodDetection) { /* integers are out of order to preserve existing preferences */ TXFileTransferIPAddressMethodRouterOnly NS_SWIFT_NAME(routerOnly) = 3, TXFileTransferIPAddressMethodRouterAndFirstParty NS_SWIFT_NAME(routerAndFirstParty) = 1, TXFileTransferIPAddressMethodRouterAndThirdParty NS_SWIFT_NAME(routerAndThirdParty) = 4, TXFileTransferIPAddressMethodManual NS_SWIFT_NAME(manual) = 2, }; typedef NS_ENUM(NSUInteger, TXChannelViewArrangement) { TXChannelViewArrangedHorizontally NS_SWIFT_NAME(horizontal) = 0, TXChannelViewArrangedVertically NS_SWIFT_NAME(vertical) = 1 }; typedef NS_ENUM(NSUInteger, TXPreferredAppearance) { TXPreferredAppearanceInherited = 0, TXPreferredAppearanceLight = 1, TXPreferredAppearanceDark = 2 }; @interface TPCPreferences (TPCPreferencesLocal) + (BOOL)appNapEnabled; + (BOOL)developerModeEnabled; + (nullable NSString *)masqueradeCTCPVersion; #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 + (BOOL)receiveBetaUpdates; #endif + (BOOL)channelNavigationIsServerSpecific; + (BOOL)automaticallyDetectHighlightSpam; + (BOOL)rememberServerListQueryStates; + (TVCMainWindowTextViewFontSize)mainTextViewFontSize; + (BOOL)focusMainTextViewOnSelectionChange; + (BOOL)logToDisk; // Checks whether checkbox for logging is checked. + (BOOL)logToDiskIsEnabled; // Checks whether checkbox is checked and whether an actual path is configured. + (BOOL)postNotificationsWhileInFocus; + (BOOL)automaticallyFilterUnicodeTextSpam; + (BOOL)conversationTrackingIncludesUserModeSymbol; + (NSString *)defaultRealName; + (NSString *)defaultUsername; + (NSString *)defaultNickname; + (nullable NSString *)defaultAwayNickname; + (NSString *)defaultKickMessage; + (NSString *)IRCopDefaultKillMessage; + (NSString *)IRCopDefaultGlineMessage; + (NSString *)IRCopDefaultShunMessage; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 + (BOOL)textEncryptionIsOpportunistic; + (BOOL)textEncryptionIsRequired; + (BOOL)textEncryptionIsEnabled; #endif + (BOOL)enableEchoMessageCapability; + (BOOL)giveFocusOnMessageCommand; + (BOOL)amsgAllConnections; + (BOOL)awayAllConnections; + (BOOL)nickAllConnections; + (BOOL)clearAllConnections; + (BOOL)memberListSortFavorsServerStaff; + (BOOL)memberListUpdatesUserInfoPopoverOnScroll; + (BOOL)memberListDisplayNoModeSymbol; + (TXNoticeSendLocation)locationToSendNotices; + (BOOL)disableNicknameColorHashing; + (BOOL)displayDockBadge; + (BOOL)displayPublicMessageCountOnDockBadge; + (BOOL)setAwayOnScreenSleep; + (BOOL)disconnectOnSleep; + (BOOL)preferModernCiphers; + (BOOL)preferModernSockets; + (BOOL)autoAddScrollbackMark; + (BOOL)showDateChanges; + (BOOL)showInlineMedia; + (BOOL)showJoinLeave; + (BOOL)displayServerMOTD; + (BOOL)rightToLeftFormatting; + (BOOL)removeAllFormatting; + (NSUInteger)trackUserAwayStatusMaximumChannelSize; + (TXPreferredAppearance)appearance; + (BOOL)disableSidebarTranslucency; + (BOOL)hideMainWindowSegmentedController; + (BOOL)reloadScrollbackOnLaunch; + (BOOL)automaticallyReloadCustomThemesWhenTheyChange; + (BOOL)autoJoinOnInvite; + (BOOL)confirmQuit; + (BOOL)rejoinOnKick; + (BOOL)copyOnSelect; + (BOOL)replyToCTCPRequests; + (BOOL)inputHistoryIsChannelSpecific; + (TXCommandWKeyAction)commandWKeyAction; + (BOOL)commandReturnSendsMessageAsAction; + (BOOL)controlEnterSendsMessage; + (BOOL)openBrowserInBackground; + (BOOL)connectOnDoubleclick; + (BOOL)disconnectOnDoubleclick; + (BOOL)joinOnDoubleclick; + (BOOL)leaveOnDoubleclick; + (NSUInteger)autojoinMaximumChannelJoins; + (NSTimeInterval)autojoinDelayBetweenChannelJoins; + (NSTimeInterval)autojoinDelayAfterIdentification; + (TXUserDoubleClickAction)userDoubleClickOption; + (TXHostmaskBanFormat)banFormat; + (BOOL)webKit2Enabled; + (BOOL)webKit2ProcessPoolSizeLimited; + (BOOL)webKit2PreviewLinks; + (NSString *)themeName; + (NSString *)themeNameDefault; + (NSString *)themeNicknameFormat; + (NSString *)themeNicknameFormatDefault; + (NSString *)themeTimestampFormat; + (NSString *)themeTimestampFormatDefault; + (nullable NSString *)themeUserStyleSheetRules; + (CGFloat)mainWindowTransparency; + (nullable NSFont *)themeChannelViewFont; + (NSString *)themeChannelViewFontName; + (NSString *)themeChannelViewFontNameDefault; + (CGFloat)themeChannelViewFontSize; + (BOOL)themeNicknameFormatPreferenceUserConfigurable; + (BOOL)themeTimestampFormatPreferenceUserConfigurable; + (BOOL)themeChannelViewFontPreferenceUserConfigurable; + (BOOL)themeChannelViewUsesCustomScrollers; + (NSUInteger)scrollbackSaveLimit; + (NSUInteger)scrollbackVisibleLimit; + (TXChannelViewArrangement)channelViewArrangement; + (BOOL)soundIsMuted; + (BOOL)onlySpeakEventsForSelection; + (BOOL)channelMessageSpeakChannelName; + (BOOL)channelMessageSpeakNickname; + (nullable NSString *)soundForEvent:(TXNotificationType)event; + (BOOL)speakEvent:(TXNotificationType)event; + (BOOL)notificationEnabledForEvent:(TXNotificationType)event; + (BOOL)disabledWhileAwayForEvent:(TXNotificationType)event; + (BOOL)bounceDockIconForEvent:(TXNotificationType)event; + (BOOL)bounceDockIconRepeatedlyForEvent:(TXNotificationType)event; + (TXTabKeyAction)tabKeyAction; + (BOOL)fileTransferRequestsAreReversed; + (BOOL)fileTransfersPreventIdleSystemSleep; + (TXFileTransferRequestReply)fileTransferRequestReplyAction; + (TXFileTransferIPAddressMethodDetection)fileTransferIPAddressDetectionMethod; + (uint16_t)fileTransferPortRangeStart; + (uint16_t)fileTransferPortRangeEnd; + (nullable NSString *)fileTransferManuallyEnteredIPAddress; + (nullable NSString *)fileTransferIPAddressInterfaceName; + (nullable NSString *)tabCompletionSuffix; + (BOOL)tabCompletionDoNotAppendWhitespace; + (BOOL)tabCompletionCutForwardToFirstWhitespace; + (nullable NSArray *)clientList; + (TXNicknameHighlightMatchType)highlightMatchingMethod; + (BOOL)logHighlights; + (BOOL)highlightCurrentNickname; + (CGFloat)swipeMinimumLength; + (nullable NSArray *)highlightMatchKeywords; + (nullable NSArray *)highlightExcludeKeywords; + (NSDictionary *)defaultPreferences; + (BOOL)textFieldAutomaticSpellCheck; + (BOOL)textFieldAutomaticGrammarCheck; + (BOOL)textFieldAutomaticSpellCorrection; + (BOOL)textFieldSmartCopyPaste; + (BOOL)textFieldSmartQuotes; + (BOOL)textFieldSmartDashes; + (BOOL)textFieldSmartLinks; + (BOOL)textFieldDataDetectors; + (BOOL)textFieldTextReplacement; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCPreferencesReload.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferences.h" NS_ASSUME_NONNULL_BEGIN typedef NS_OPTIONS(NSUInteger, TPCPreferencesReloadAction) { TPCPreferencesReloadActionAppearance = 1 << 0, TPCPreferencesReloadActionChannelViewArrangement = 1 << 1, TPCPreferencesReloadActionDockIconBadges = 1 << 2, TPCPreferencesReloadActionHighlightKeywords = 1 << 3, TPCPreferencesReloadActionHighlightLogging = 1 << 4, TPCPreferencesReloadActionIRCCommandCache = 1 << 5, TPCPreferencesReloadActionInputHistoryScope = 1 << 6, TPCPreferencesReloadActionLogTranscripts = 1 << 7, TPCPreferencesReloadActionMainWindowTransparencyLevel = 1 << 8, TPCPreferencesReloadActionMemberList = 1 << 9, TPCPreferencesReloadActionMemberListSortOrder = 1 << 10, TPCPreferencesReloadActionMemberListUserBadges = 1 << 11, TPCPreferencesReloadActionPreferencesChanged = 1 << 12, TPCPreferencesReloadActionScrollbackSaveLimit = 1 << 13, TPCPreferencesReloadActionScrollbackVisibleLimit = 1 << 14, TPCPreferencesReloadActionServerList = 1 << 15, TPCPreferencesReloadActionServerListUnreadBadges = 1 << 16, TPCPreferencesReloadActionStyle = 1 << 17, // TPCPreferencesReloadActionStyleWithTableViews = 1 << 18, TPCPreferencesReloadActionTextDirection = 1 << 19, TPCPreferencesReloadActionTextFieldFontSize = 1 << 20, TPCPreferencesReloadActionTextFieldSegmentedControllerOrigin = 1 << 21, #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 TPCPreferencesReloadActionEncryptionPolicy = 1 << 22, #endif #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 TPCPreferencesReloadActionSparkleFrameworkFeedURL = 1 << 23, #endif }; @interface TPCPreferences (TPCPreferencesReload) + (void)performReloadActionForKeys:(NSArray *)keys; + (void)performReloadAction:(TPCPreferencesReloadAction)reloadAction; + (void)performReloadAction:(TPCPreferencesReloadAction)reloadAction forKey:(nullable NSString *)key; // key is only used for context @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCPreferencesUserDefaultsLocal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferencesUserDefaults.h" NS_ASSUME_NONNULL_BEGIN @interface TPCPreferencesUserDefaults (TPCPreferencesUserDefaultsLocal) + (BOOL)keyIsExcludedFromExportImport:(NSString *)defaultName; + (BOOL)keyIsExcludedFromMigration:(NSString *)defaultName; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCResourceManager.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN TEXTUAL_EXTERN NSString * const TPCResourceManagerBundleDocumentTypeExtension; TEXTUAL_EXTERN NSString * const TPCResourceManagerBundleDocumentTypeExtensionWithoutPeriod; TEXTUAL_EXTERN NSString * const TPCResourceManagerScriptDocumentTypeExtension; TEXTUAL_EXTERN NSString * const TPCResourceManagerScriptDocumentTypeExtensionWithoutPeriod; @interface TPCResourceManager : NSObject /* Open a property list file in the Resources folder of Textual named `name` in optional subdirectory `subpath`. If `key` is specified, then the object value of `key` is returned from the root object of the property list as long as that object is a dictionary. The root object is returned if `key` is not specified if it is a dictionary. The value of this returned object is cached unless `cacheValue` is NO. Cache is also bypassed when `cacheValue` is NO. */ + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name; + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name cacheValue:(BOOL)cacheValue; + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath; + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath cacheValue:(BOOL)cacheValue; + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name key:(nullable NSString *)key; + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name key:(nullable NSString *)key cacheValue:(BOOL)cacheValue; + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key; + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key cacheValue:(BOOL)cacheValue; /* Open a property list file in the Resources folder of Textual named `name` in optional subdirectory `subpath`. If `key` is specified, then the object value of `key` is returned from the root object of the property list as long as that object is an array. The root object is returned if `key` is not specified if it is an array. The value of this returned object is cached unless `cacheValue` is NO. Cache is also bypassed when `cacheValue` is NO. */ + (nullable NSArray *)arrayFromResources:(NSString *)name; + (nullable NSArray *)arrayFromResources:(NSString *)name cacheValue:(BOOL)cacheValue; + (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath; + (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath cacheValue:(BOOL)cacheValue; + (nullable NSArray *)arrayFromResources:(NSString *)name key:(nullable NSString *)key; + (nullable NSArray *)arrayFromResources:(NSString *)name key:(nullable NSString *)key cacheValue:(BOOL)cacheValue; + (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key; + (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key cacheValue:(BOOL)cacheValue; /* Open a property list file in the Resources folder of Textual named `name` in optional subdirectory `subpath`. If `key` is specified, then the object value of `key` is returned from the root object of the property list as long as that object is kind of `class`. The root object is returned if `key` is not specified if it is kind of `class`. The value of this returned object is cached unless `cacheValue` is NO. Cache is also bypassed when `cacheValue` is NO. */ + (nullable id)objectFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key kindOf:(Class)class cacheValue:(BOOL)cacheValue; /* This cache object is used for loading resources using the methods above. The key used for each cache entry is an implementation detail and is subject to change. */ @property (class, strong, readonly) NSCache *sharedResourcesCache; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCTheme.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCLogLine.h" NS_ASSUME_NONNULL_BEGIN #define TPCThemeSettingsDisabledIndentationOffset -99 #define TPCThemeSettingsNewestTemplateEngineVersion 4 typedef NS_ENUM(NSUInteger, TPCThemeAppearanceType) { TPCThemeAppearanceTypeDefault = 0, // Automatically picked based on window appearance TPCThemeAppearanceTypeDark, TPCThemeAppearanceTypeLight }; typedef NS_ENUM(NSUInteger, TPCThemeStorageLocation) { TPCThemeStorageLocationUnknown = 0, TPCThemeStorageLocationBundle, TPCThemeStorageLocationCustom }; typedef NS_ENUM(NSUInteger, TPCThemeSettingsNicknameColorStyle) { TPCThemeSettingsNicknameColorStyleDefault = 0, // Automatically picked based on appearance TPCThemeSettingsNicknameColorStyleDark, TPCThemeSettingsNicknameColorStyleLight }; /* If a theme is modified in such a way after it is initialized that it can no longer be used, then this notification is posted. A way, amongst many, in which the integrity of a theme can be compromised is by deleting the CSS or JavaScript file. */ TEXTUAL_EXTERN NSNotificationName const TPCThemeIntegrityCompromisedNotification; /* If theme has been restored to a usable state. */ TEXTUAL_EXTERN NSNotificationName const TPCThemeIntegrityRestoredNotification; /* If the theme has been deleted. Drop reference to theme object when this occurs. Holding a reference to a theme object after it has been deleted can result in undefined behavior especially if another theme is installed using the same URL. */ TEXTUAL_EXTERN NSNotificationName const TPCThemeWasDeletedNotification; /* The theme can change the variety to match appearance changes, or when one variety becomes compromised and another must be used. */ /* Notification used for first case. */ TEXTUAL_EXTERN NSNotificationName const TPCThemeAppearanceChangedNotification; /* Notification used for second case. */ TEXTUAL_EXTERN NSNotificationName const TPCThemeVarietyChangedNotification; /* A CSS or JavaScript file within the global variety or the variety in use was modified. */ TEXTUAL_EXTERN NSNotificationName const TPCThemeWasModifiedNotification; @class GRMustacheTemplate, GRMustacheTemplateRepository; @class TPCThemeSettings; @interface TPCTheme : NSObject @property (readonly, copy) NSString *name; @property (readonly, copy) NSURL *originalURL; @property (readonly) TPCThemeStorageLocation storageLocation; @property (readonly) BOOL usable; // If the theme is in a state that can be selected by the user. @property (readonly) TPCThemeAppearanceType appearance; /* Global files are listed first with variety specific files second. */ /* These properties DO NOT list all files of these types. Only files named "design.css" and "scripts.js" respectively. */ @property (readonly, copy) NSArray *cssFiles; @property (readonly, copy) NSArray *jsFiles; @property (readonly, copy) NSArray *cssFilePaths; @property (readonly, copy) NSArray *jsFilePaths; /* Order of repositories is: variety specific -> global -> app */ @property (readonly, copy) NSArray *templateRepositories; /* Settings */ @property (readonly, strong) TPCThemeSettings *settings; /* Temporary location */ /* Themes are copied to a temporary location when they are in use. */ /* These properties remap the relevant URLs to the temporary location. */ /* These files will not exist until the theme is in use. */ @property (readonly, copy) NSURL *temporaryURL; @property (readonly, copy) NSArray *temporaryCSSFiles; @property (readonly, copy) NSArray *temporaryJSFiles; @property (readonly, copy) NSArray *temporaryCSSFilePaths; @property (readonly, copy) NSArray *temporaryJSFilePaths; - (instancetype)init NS_UNAVAILABLE; /* Templates */ - (nullable GRMustacheTemplate *)templateWithLineType:(TVCLogLineType)type; - (nullable GRMustacheTemplate *)templateWithName:(NSString *)name; @end @interface TPCThemeSettings : NSObject @property (readonly) TPCThemeAppearanceType appearance; @property (readonly) BOOL invertSidebarColors; @property (readonly) BOOL js_postHandleEventNotifications; @property (readonly) BOOL js_postAppearanceChangesNotification; @property (readonly) BOOL js_postPreferencesDidChangesNotifications; @property (readonly) BOOL usesIncompatibleTemplateEngineVersion; @property (readonly, copy, nullable) NSFont *themeChannelViewFont; @property (readonly, copy, nullable) NSString *themeNicknameFormat; @property (readonly, copy, nullable) NSString *themeTimestampFormat; @property (readonly, copy, nullable) NSString *settingsKeyValueStoreName; @property (readonly, copy, nullable) NSColor *channelViewOverlayColor; @property (readonly, copy, nullable) NSColor *underlyingWindowColor; @property (readonly) BOOL underlyingWindowColorIsDark; @property (readonly) double indentationOffset; @property (readonly) TPCThemeSettingsNicknameColorStyle nicknameColorStyle; @property (readonly) NSUInteger templateEngineVersion; - (instancetype)init NS_UNAVAILABLE; - (nullable id)styleSettingsRetrieveValueForKey:(NSString *)key error:(NSString * _Nullable * _Nullable)resultError; - (BOOL)styleSettingsSetValue:(nullable id)objectValue forKey:(NSString *)objectKey error:(NSString * _Nullable * _Nullable)resultError; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TPCThemeController.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCTheme.h" NS_ASSUME_NONNULL_BEGIN TEXTUAL_EXTERN NSString * const TPCThemeControllerCustomThemeNameBasicPrefix; TEXTUAL_EXTERN NSString * const TPCThemeControllerCustomThemeNameCompletePrefix; TEXTUAL_EXTERN NSString * const TPCThemeControllerBundledThemeNameBasicPrefix; TEXTUAL_EXTERN NSString * const TPCThemeControllerBundledThemeNameCompletePrefix; TEXTUAL_EXTERN NSNotificationName const TPCThemeControllerThemeListDidChangeNotification; /* Theme is not loaded until main window is woken which means while you could in theory access this object before then, objects below that are marked non-nil will actually be nil. */ @interface TPCThemeController : NSObject @property (readonly, strong) TPCTheme *theme; @property (readonly, strong) TPCThemeSettings *settings; @property (readonly) TPCThemeStorageLocation storageLocation; @property (readonly, copy) NSString *name; @property (readonly, copy) NSURL *originalURL; // Where original copy of theme is. @property (readonly, copy) NSURL *temporaryURL; // Where cached copy of theme is. @property (readonly, copy) NSString *originalPath; @property (readonly, copy) NSString *temporaryPath; @property (readonly, copy) NSString *cacheToken; @property (getter=isBundledTheme, readonly) BOOL bundledTheme; /* Calls for all themes */ - (void)enumerateAvailableThemesWithBlock:(void(NS_NOESCAPE ^)(NSString *fileName, TPCThemeStorageLocation storageLocation, BOOL multipleVariants, BOOL *stop))enumerationBlock; - (BOOL)themeExists:(NSString *)themeName; + (nullable NSString *)pathOfThemeWithName:(NSString *)themeName; + (nullable NSString *)pathOfThemeWithName:(NSString *)themeName storageLocation:(nullable TPCThemeStorageLocation *)storageLocation; + (nullable NSString *)buildFilename:(NSString *)name forStorageLocation:(TPCThemeStorageLocation)storageLocation; + (nullable NSString *)extractThemeSource:(NSString *)source; + (nullable NSString *)extractThemeName:(NSString *)source; + (TPCThemeStorageLocation)storageLocationOfThemeWithName:(NSString *)themeName; + (nullable NSString *)descriptionForStorageLocation:(TPCThemeStorageLocation)storageLocation; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCAlert.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN /* TVCAlert acts as a non-blocking substitute to NSAlert which can be used to show messages that aren't important. */ typedef NS_ENUM(NSUInteger, TVCAlertResponseButton) { TVCAlertResponseButtonFirst = 1000, TVCAlertResponseButtonSecond = 1001, TVCAlertResponseButtonThird = 1002 }; typedef NS_ENUM(NSUInteger, TVCAlertType) { /* Standard alert */ TVCAlertTypeInformational = 0, /* Warning icon appears above icon. No other difference. */ TVCAlertTypeWarning }; @class TVCAlert; typedef void (^TVCAlertCompletionBlock)(TVCAlert *sender, TVCAlertResponseButton buttonClicked); typedef BOOL (^TVCAlertButtonClickedBlock)(TVCAlert *sender, TVCAlertResponseButton buttonClicked); @interface TVCAlert : NSObject /* All properties are immutable once alert is visible */ @property (nonatomic, copy) NSString *messageText; @property (nonatomic, copy) NSString *informativeText; @property (nonatomic, strong, null_resettable) NSImage *icon; @property (nonatomic, assign) TVCAlertType type; - (NSButton *)setTitle:(NSString *)title forButton:(TVCAlertResponseButton)button; - (NSButton *)setTitle:(NSString *)title forButtonAtIndex:(NSUInteger)index; @property (copy, readonly) NSArray *buttons; @property (nonatomic, assign) BOOL showsSuppressionButton; @property (readonly, weak) NSButton *suppressionButton; @property (nonatomic, strong, nullable) NSView *accessoryView; @property (readonly, strong) NSWindow *window; - (void)showAlert; - (void)showAlertWithCompletionBlock:(nullable TVCAlertCompletionBlock)completionBlock; - (void)showAlertInWindow:(NSWindow *)window; - (void)showAlertInWindow:(NSWindow *)window withCompletionBlock:(nullable TVCAlertCompletionBlock)completionBlock; - (TVCAlertResponseButton)runModal; /* A block that is called when a button is clicked. If the block returns YES, then the alert is dismissed. If the block returns NO, then no action is taken. This feature allows you to take action when a button is clicked, while keeping it visible to the user, without the need to override the target / action of the button in question. */ /* Changing a button clicked block is permitted with an alert visible. */ /* An exception is thrown for out-of-bounds access so call this AFTER a button at the index is added. */ - (void)setButtonClickedBlock:(nullable TVCAlertButtonClickedBlock)block forButton:(TVCAlertResponseButton)button; - (void)setButtonClickedBlock:(nullable TVCAlertButtonClickedBlock)block forButtonAtIndex:(NSUInteger)index; /* End alert */ - (void)endAlert; // TVCAlertResponseButtonFirst - (void)endAlertWithResponse:(TVCAlertResponseButton)response; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCAppearance.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXAppearance.h" NS_ASSUME_NONNULL_BEGIN @interface TVCAppearance : NSObject - (instancetype)init NS_UNAVAILABLE; /* Top level group */ /* Nonnull until -flushAppearanceProperties is called. */ @property (readonly, copy, nullable) NSDictionary *appearanceProperties; /* Properties */ @property (readonly) BOOL isHighResolutionAppearance; /* Stateless Accessors */ - (nullable NSColor *)colorForKey:(NSString *)key; - (nullable NSColor *)colorInGroup:(NSDictionary *)group withKey:(NSString *)key; - (nullable NSGradient *)gradientForKey:(NSString *)key; - (nullable NSGradient *)gradientInGroup:(NSDictionary *)group withKey:(NSString *)key; - (nullable NSFont *)fontForKey:(NSString *)key; - (nullable NSFont *)fontInGroup:(NSDictionary *)group withKey:(NSString *)key; - (nullable NSImage *)imageForKey:(NSString *)key; - (nullable NSImage *)imageInGroup:(NSDictionary *)group withKey:(NSString *)key; - (NSSize)sizeForKey:(NSString *)key; - (NSSize)sizeInGroup:(NSDictionary *)group withKey:(NSString *)key; - (CGFloat)measurementForKey:(NSString *)key; - (CGFloat)measurementInGroup:(NSDictionary *)group withKey:(NSString *)key; /* Stateful Accessors */ /* Stateful appearance properties require the properties to have a "activeWindow" and "inactiveWindow" dictionary value which contains the value of the property itself. */ /* Example: exampleStatefulColor activeWindow type 1 value 0.0 0.3 inactiveWindow type 1 value 1.0 */ - (nullable NSColor *)colorForKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow; - (nullable NSColor *)colorInGroup:(NSDictionary *)group withKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow; - (nullable NSGradient *)gradientForKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow; - (nullable NSGradient *)gradientInGroup:(NSDictionary *)group withKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow; - (nullable NSFont *)fontForKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow; - (nullable NSFont *)fontInGroup:(NSDictionary *)group withKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow; - (nullable NSImage *)imageForKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow; - (nullable NSImage *)imageInGroup:(NSDictionary *)group withKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow; @end /* TVCApplicationAppearance keeps a copy of TXAppearance properties. The properties it keeps are those that were set when the object was created. To update to the latest properties, create a new instance of the object. */ @interface TVCApplicationAppearance : TVCAppearance @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCAutoExpandingTextField.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCAutoExpandingTextField : NSTextField @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCAutoExpandingTokenField.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCAutoExpandingTokenField : NSTokenField @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCBasicTableView.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCBasicTableView : NSTableView @property (nonatomic, weak) id pasteboardDelegate; @property (nonatomic, weak) id textEditingDelegate; @property (readonly, assign) BOOL presentMenuForEmptySelection; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCChannelSelectionViewController.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @protocol TVCChannelSelectionViewControllerDelegate; @interface TVCChannelSelectionViewController : NSObject @property (nonatomic, weak) id delegate; @property (nonatomic, copy) NSArray *selectedClientIds; @property (nonatomic, copy) NSArray *selectedChannelIds; - (void)attachToView:(NSView *)view; @end #pragma mark - @protocol TVCChannelSelectionViewControllerDelegate @required - (void)channelSelectionControllerSelectionChanged:(TVCChannelSelectionViewController *)controller; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCLogController.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel; @class TVCLogLine, TVCLogView, TVCMainWindow; TEXTUAL_EXTERN NSNotificationName const TVCLogControllerViewFinishedLoadingNotification; @interface TVCLogController : NSObject @property (readonly) TVCLogView *backingView; @property (readonly, getter=viewIsEncrypted) BOOL encrypted; @property (readonly, getter=viewIsLoaded) BOOL loaded; @property (readonly, getter=viewIsSelected) BOOL selected; @property (readonly, getter=viewIsVisible) BOOL visible; @property (readonly) NSUInteger numberOfLines; @property (readonly, weak) IRCClient *associatedClient; @property (readonly, weak) IRCChannel *associatedChannel; @property (readonly, weak) TVCMainWindow *attachedWindow; @property (readonly, copy, nullable) NSString *newestLineNumberFromPreviousSession; @property (readonly, copy, nullable) NSString *oldestLineNumber; @property (readonly, copy, nullable) NSString *newestLineNumber; - (instancetype)init NS_UNAVAILABLE; - (void)nextHighlight; - (void)previousHighlight; - (BOOL)highlightAvailable:(BOOL)previous; @property (readonly, copy) NSString *uniqueIdentifier; - (void)moveToTop; - (void)moveToBottom; - (void)jumpToCurrentSession; - (void)jumpToPresent; - (void)jumpToLine:(NSString *)lineNumber; - (void)jumpToLine:(NSString *)lineNumber completionHandler:(void (^ _Nullable)(BOOL result))completionHandler; - (void)setTopic:(nullable NSString *)topic; @property (readonly) BOOL inlineMediaEnabledForView; - (void)mark; - (void)unmark; - (void)goToMark; - (void)clear; - (void)changeTextSize:(BOOL)bigger; - (void)evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments; // Defaults to onQueue YES - (void)evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments onQueue:(BOOL)onQueue; @end #pragma mark - @interface TVCLogControllerPrintOperationContext : NSObject @property (readonly, weak) IRCClient *client; @property (readonly, weak) IRCChannel *channel; @property (readonly, getter=isHighlight) BOOL highlight; @property (readonly, copy) TVCLogLine *logLine; @property (readonly, copy) NSString *lineNumber; @end typedef void (^TVCLogControllerPrintOperationCompletionBlock)(TVCLogControllerPrintOperationContext *context); NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCLogLine.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCChannel; TEXTUAL_EXTERN NSString * const TVCLogLineUndefinedNicknameFormat; TEXTUAL_EXTERN NSString * const TVCLogLineActionNicknameFormat; TEXTUAL_EXTERN NSString * const TVCLogLineNoticeNicknameFormat; TEXTUAL_EXTERN NSString * const TVCLogLineSpecialNoticeMessageFormat; TEXTUAL_EXTERN NSString * const TVCLogLineDefaultCommandValue; typedef NS_ENUM(NSUInteger, TVCLogLineType) { TVCLogLineTypeUndefined = 0, TVCLogLineTypeAction, TVCLogLineTypeActionNoHighlight, TVCLogLineTypeCTCP, TVCLogLineTypeCTCPQuery, TVCLogLineTypeCTCPReply, TVCLogLineTypeDCCFileTransfer, TVCLogLineTypeDebug, TVCLogLineTypeInvite, TVCLogLineTypeJoin, TVCLogLineTypeKick, TVCLogLineTypeKill, TVCLogLineTypeMode, TVCLogLineTypeNick, TVCLogLineTypeNotice, TVCLogLineTypeOffTheRecordEncryptionStatus, TVCLogLineTypePart, TVCLogLineTypePrivateMessage, TVCLogLineTypePrivateMessageNoHighlight, TVCLogLineTypeQuit, TVCLogLineTypeTopic, TVCLogLineTypeWebsite, }; typedef NS_ENUM(NSUInteger, TVCLogLineMemberType) { TVCLogLineMemberTypeNormal = 0, TVCLogLineMemberTypeLocalUser, }; #define IRCCommandFromLineType(t) [TVCLogLine stringForLineType:t] #pragma mark - #pragma mark Immutable Object @interface TVCLogLine : XRPortablePropertyObject @property (readonly) BOOL isEncrypted; @property (readonly) BOOL isFirstForDay; // // YES if is first line for the day defined by receivedAt @property (readonly, copy) NSDate *receivedAt; @property (readonly, copy) NSString *nicknameColorStyle; @property (readonly) BOOL nicknameColorStyleOverride; // YES if the nicknameColorStyle was set by the user @property (readonly, copy, nullable) NSString *nickname; @property (readonly, copy) NSString *messageBody; @property (readonly, copy) NSString *command; // Can be the actual command (PRIVMSG, NOTICE, etc.) or the raw numeric (001, 002, etc.) @property (readonly, copy) NSString *uniqueIdentifier; @property (readonly) TVCLogLineType lineType; @property (readonly) TVCLogLineMemberType memberType; @property (readonly, copy, nullable) NSArray *highlightKeywords; @property (readonly, copy, nullable) NSArray *excludeKeywords; @property (readonly, copy, nullable) NSDictionary *rendererAttributes; @property (readonly) NSUInteger sessionIdentifier; - (nullable instancetype)initWithData:(NSData *)data NS_DESIGNATED_INITIALIZER; @property (readonly, copy) NSString *formattedTimestamp; @property (readonly, copy) NSString *formattedNickname; - (nullable NSString *)formattedNicknameInChannel:(nullable IRCChannel *)channel; @property (readonly, copy, nullable) NSString *lineTypeString; @property (readonly, copy) NSString *memberTypeString; + (nullable NSString *)stringForLineType:(TVCLogLineType)type; + (NSString *)stringForMemberType:(TVCLogLineMemberType)type; @end #pragma mark - #pragma mark Mutable Object @interface TVCLogLineMutable : TVCLogLine @property (nonatomic, assign, readwrite) BOOL isEncrypted; @property (nonatomic, assign, readwrite) BOOL isFirstForDay; @property (nonatomic, copy, readwrite) NSDate *receivedAt; @property (nonatomic, copy, readwrite, nullable) NSString *nickname; @property (nonatomic, copy, readwrite) NSString *messageBody; @property (nonatomic, copy, readwrite) NSString *command; @property (nonatomic, assign, readwrite) TVCLogLineType lineType; @property (nonatomic, assign, readwrite) TVCLogLineMemberType memberType; @property (nonatomic, copy, readwrite, nullable) NSArray *highlightKeywords; @property (nonatomic, copy, readwrite, nullable) NSArray *excludeKeywords; @property (nonatomic, copy, readwrite, nullable) NSDictionary *rendererAttributes; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCLogRenderer.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class GRMustacheTemplate; @class TVCLogController; typedef NSString *TVCLogRendererConfigurationAttribute NS_STRING_ENUM; typedef NSString *TVCLogRendererResultsAttribute NS_STRING_ENUM; /* Properties to configure the renderer and provide additional context so that it can provide the best possible results. */ /* These properties do not apply to attributed strings */ TEXTUAL_EXTERN TVCLogRendererConfigurationAttribute const TVCLogRendererConfigurationRenderLinksAttribute; // BOOL TEXTUAL_EXTERN TVCLogRendererConfigurationAttribute const TVCLogRendererConfigurationLineTypeAttribute; // TVCLogLineType TEXTUAL_EXTERN TVCLogRendererConfigurationAttribute const TVCLogRendererConfigurationMemberTypeAttribute; // TVCLogMemberType TEXTUAL_EXTERN TVCLogRendererConfigurationAttribute const TVCLogRendererConfigurationHighlightKeywordsAttribute; // NSArray TEXTUAL_EXTERN TVCLogRendererConfigurationAttribute const TVCLogRendererConfigurationExcludedKeywordsAttribute; // NSArray TEXTUAL_EXTERN TVCLogRendererConfigurationAttribute const TVCLogRendererConfigurationDoNotEscapeBodyAttribute; // BOOL /* These properties apply to attributed strings */ TEXTUAL_EXTERN TVCLogRendererConfigurationAttribute const TVCLogRendererConfigurationAttributedStringPreferredFontAttribute; // NSFont TEXTUAL_EXTERN TVCLogRendererConfigurationAttribute const TVCLogRendererConfigurationAttributedStringPreferredFontColorAttribute; // NSColor /* Properties that are returned in the outputDictionary of a render */ /* The output dictionary is not guaranteed to contain any key. */ TEXTUAL_EXTERN TVCLogRendererResultsAttribute const TVCLogRendererResultsListOfLinksInBodyAttribute; // NSArray TEXTUAL_EXTERN TVCLogRendererResultsAttribute const TVCLogRendererResultsListOfLinksMappedInBodyAttribute; // NSDictionary TEXTUAL_EXTERN TVCLogRendererResultsAttribute const TVCLogRendererResultsKeywordMatchFoundAttribute; // BOOL TEXTUAL_EXTERN TVCLogRendererResultsAttribute const TVCLogRendererResultsListOfUsersFoundAttribute; // NSSet TEXTUAL_EXTERN TVCLogRendererResultsAttribute const TVCLogRendererResultsOriginalBodyWithoutEffectsAttribute; // NSString @interface TVCLogRenderer : NSObject + (NSString *)escapeHTML:(NSString *)html; + (nullable NSColor *)mapColor:(id)color; + (NSColor *)mapColorCode:(NSUInteger)colorCode; + (nullable NSString *)renderTemplateNamed:(NSString *)templateName; + (nullable NSString *)renderTemplateNamed:(NSString *)templateName attributes:(nullable NSDictionary *)templateTokens; + (nullable NSString *)renderTemplate:(GRMustacheTemplate *)template; + (nullable NSString *)renderTemplate:(GRMustacheTemplate *)template attributes:(nullable NSDictionary *)templateTokens; + (NSAttributedString *)renderBodyAsAttributedString:(NSString *)body withAttributes:(NSDictionary *)inputDictionary; + (NSString *)renderBody:(NSString *)body forViewController:(TVCLogController *)viewController withAttributes:(NSDictionary *)inputDictionary resultInfo:(NSDictionary * _Nullable * _Nullable)outputDictionary; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCLogView.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN TEXTUAL_EXTERN NSString * const TVCLogViewCommonUserAgentString; @interface TVCLogView : NSObject @property (readonly) BOOL hasSelection; - (void)clearSelection; @property (readonly, copy, nullable) NSString *selection; /* [TPCPreferences webKit2Enabled] will return YES as long as the preference to enable WebKit2 is enabled. */ /* [TVCLogView webKit2Enabled] will reflect the value of [TPCPreferences webKit2Enabled] until such time that a WebKit2 process closes unexpectedly. Such as a crash. It will then return NO until the app is relaunched. */ @property (readonly, class) BOOL webKit2Enabled; @property (readonly) BOOL isUsingWebKit2; @property (readonly) NSView *webView; @property (readonly, getter=isLayingOutView) BOOL layingOutView; - (instancetype)init NS_UNAVAILABLE; @end @interface TVCLogView (TVCLogViewJavaScriptHandler) - (void)evaluateJavaScript:(NSString *)code; - (void)evaluateJavaScript:(NSString *)code completionHandler:(void (^ _Nullable)(id _Nullable result))completionHandler; - (void)evaluateFunction:(NSString *)function; - (void)evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments; - (void)evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(id _Nullable result))completionHandler; - (void)booleanByEvaluatingFunction:(NSString *)function completionHandler:(void (^ _Nullable)(BOOL result))completionHandler; - (void)booleanByEvaluatingFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(BOOL result))completionHandler; - (void)stringByEvaluatingFunction:(NSString *)function completionHandler:(void (^ _Nullable)(NSString * _Nullable result))completionHandler; - (void)stringByEvaluatingFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(NSString * _Nullable result))completionHandler; - (void)arrayByEvaluatingFunction:(NSString *)function completionHandler:(void (^ _Nullable)(NSArray * _Nullable result))completionHandler; - (void)arrayByEvaluatingFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(NSArray * _Nullable result))completionHandler; - (void)dictionaryByEvaluatingFunction:(NSString *)function completionHandler:(void (^ _Nullable)(NSDictionary * _Nullable result))completionHandler; - (void)dictionaryByEvaluatingFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(NSDictionary * _Nullable result))completionHandler; + (NSString *)escapeJavaScriptString:(NSString *)string; + (NSString *)descriptionOfJavaScriptResult:(id)scriptResult; - (void)logToJavaScriptConsole:(NSString *)message, ...; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCMainWindow.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowAppearance.h" NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel, IRCTreeItem; @class TVCMainWindowLoadingScreenView; @class TVCMainWindowSplitView, TVCMainWindowTextView; @class TVCServerList, TVCMemberList; @class TVCLogController; typedef NS_ENUM(NSUInteger, TVCServerListNavigationMovementType) { TVCServerListNavigationMovementTypeAll = 0, // Move to next item TVCServerListNavigationMovementTypeActive, // Move to next active item TVCServerListNavigationMovementTypeUnread, // Move to next unread item }; typedef NS_ENUM(NSUInteger, TVCServerListNavigationSelectionType) { TVCServerListNavigationSelectionTypeAny = 0, // Move to next item TVCServerListNavigationSelectionTypeChannel, // Move to next channel item TVCServerListNavigationSelectionTypeServer, // Move to next server item }; TEXTUAL_EXTERN NSNotificationName const TVCMainWindowAppearanceChangedNotification; TEXTUAL_EXTERN NSNotificationName const TVCMainWindowRedrawSubviewsNotification; TEXTUAL_EXTERN NSNotificationName const TVCMainWindowWillReloadThemeNotification; TEXTUAL_EXTERN NSNotificationName const TVCMainWindowDidReloadThemeNotification; TEXTUAL_EXTERN NSNotificationName const TVCMainWindowSelectionChangedNotification; TEXTUAL_EXTERN NSString * const TVCServerListDragType; @interface TVCMainWindow : NSWindow @property (readonly, getter=isDisabled) BOOL disabled; @property (readonly) TVCMainWindowAppearance *userInterfaceObjects; @property (readonly, weak) TVCMainWindowLoadingScreenView *loadingScreen; @property (readonly, weak) TVCMainWindowSplitView *contentSplitView; @property (readonly, unsafe_unretained) TVCMainWindowTextView *inputTextField; @property (readonly, weak) TVCMemberList *memberList; @property (readonly, weak) TVCServerList *serverList; @property (readonly) BOOL multipleItemsSelected; @property (readonly, nullable) IRCTreeItem *selectedItem; @property (readonly, copy) NSArray *selectedItems; @property (readonly, nullable) IRCClient *selectedClient; @property (readonly, nullable) IRCChannel *selectedChannel; @property (readonly, nullable) TVCLogController *selectedViewController; @property (readonly, nullable) IRCTreeItem *previouslySelectedItem; - (void)select:(nullable IRCTreeItem *)item; - (void)selectPreviousItem; - (void)deselect:(IRCTreeItem *)item; - (void)deselectGroup:(IRCTreeItem *)item; - (BOOL)isItemVisible:(IRCTreeItem *)item; - (BOOL)isItemSelected:(IRCTreeItem *)item; - (BOOL)isItemInSelectedGroup:(IRCTreeItem *)item; - (void)expandClient:(IRCClient *)client; - (nullable IRCChannel *)selectedChannelOn:(IRCClient *)client; - (void)navigateServerEntries:(BOOL)isMovingDown withNavigationType:(TVCServerListNavigationMovementType)navigationType; - (void)navigateChannelEntries:(BOOL)isMovingDown withNavigationType:(TVCServerListNavigationMovementType)navigationType; - (void)navigateToNextEntry:(BOOL)isMovingDown; - (void)selectNextServer:(NSEvent *)e; - (void)selectNextChannel:(NSEvent *)e; - (void)selectNextWindow:(NSEvent *)e; - (void)selectPreviousServer:(NSEvent *)e; - (void)selectPreviousChannel:(NSEvent *)e; - (void)selectPreviousWindow:(NSEvent *)e; - (void)selectNextActiveServer:(NSEvent *)e; - (void)selectNextUnreadChannel:(NSEvent *)e; - (void)selectNextActiveChannel:(NSEvent *)e; - (void)selectPreviousSelection:(NSEvent *)e; - (void)selectPreviousActiveServer:(NSEvent *)e; - (void)selectPreviousUnreadChannel:(NSEvent *)e; - (void)selectPreviousActiveChannel:(NSEvent *)e; @property (getter=isUsingDarkAppearance, readonly) BOOL usingDarkAppearance; @property (readonly) double textSizeMultiplier; - (void)changeTextSize:(BOOL)bigger; - (void)markAllAsRead; - (void)markAllAsReadInGroup:(nullable IRCTreeItem *)item; - (void)reloadTheme; // reloaded asynchronously - (void)clearContentsOfClient:(IRCClient *)client; - (void)clearContentsOfChannel:(IRCChannel *)channel; - (void)clearAllViews; - (void)textEntered; @property (getter=isMemberListVisible, readonly) BOOL memberListVisible; @property (getter=isServerListVisible, readonly) BOOL serverListVisible; - (NSRect)defaultWindowFrame; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCMainWindowAppearance.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCAppearance.h" NS_ASSUME_NONNULL_BEGIN @class TVCServerListAppearance, TVCMemberListAppearance, TVCMainWindowTextViewAppearance; @interface TVCMainWindowAppearance : TVCApplicationAppearance @property (readonly) TVCServerListAppearance *serverList; @property (readonly) TVCMemberListAppearance *memberList; @property (readonly) TVCMainWindowTextViewAppearance *textView; @property (readonly) NSSize defaultWindowSize; @property (readonly, copy, nullable) NSColor *channelViewOverlayDefaultBackgroundColorActiveWindow; @property (readonly, copy, nullable) NSColor *channelViewOverlayDefaultBackgroundColorInactiveWindow; @property (readonly, copy, nullable) NSColor *loadingScreenBackgroundColor; @property (readonly, copy, nullable) NSColor *splitViewDividerColor; @property (readonly, copy, nullable) NSColor *titlebarAccessoryViewBackgroundColorActiveWindow; @property (readonly, copy, nullable) NSColor *titlebarAccessoryViewBackgroundColorInactiveWindow; @property (readonly) CGFloat titlebarAccessoryViewLeftMargin; @property (readonly) CGFloat titlebarAccessoryViewRightMargin; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCMainWindowLoadingScreen.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN /* Animation is disabled unless an animated method is invoked */ @interface TVCMainWindowLoadingScreenView : NSVisualEffectView - (void)showProgressViewWithReason:(NSString *)progressReason; - (void)setProgressViewReason:(NSString *)progressReason; - (void)showTrialExpiredView; - (void)showWelcomeAddServerView; @property (readonly) BOOL viewIsVisible; - (void)hide; - (void)hideAnimated; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCMainWindowSplitView.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowSplitView : NSSplitView - (void)expandServerList; - (void)collapseServerList; - (void)toggleServerListVisibility; @property (getter=isServerListCollapsed, readonly) BOOL serverListCollapsed; - (void)expandMemberList; - (void)collapseMemberList; - (void)toggleMemberListVisibility; @property (getter=isMemberListCollapsed, readonly) BOOL memberListCollapsed; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCMainWindowTextView.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCTextViewWithIRCFormatter.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowTextView : TVCTextViewWithIRCFormatter @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCMainWindowTextViewAppearance.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCAppearance.h" #import "TPCPreferencesLocal.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowTextViewAppearance : TVCApplicationAppearance #pragma mark - #pragma mark Text View @property (readonly) NSSize textViewInset; @property (readonly, copy, nullable) NSColor *textViewTextColor; @property (readonly, copy, nullable) NSColor *textViewPlaceholderTextColor; @property (readonly, copy, nullable) NSColor *textViewBackgroundColorActiveWindow; @property (readonly, copy, nullable) NSColor *textViewBackgroundColorInactiveWindow; @property (readonly, copy, nullable) NSColor *textViewOutlineColorActiveWindow; @property (readonly, copy, nullable) NSColor *textViewOutlineColorInactiveWindow; @property (readonly, copy, nullable) NSColor *textViewInsideShadowColorActiveWindow; @property (readonly, copy, nullable) NSColor *textViewInsideShadowColorInactiveWindow; @property (readonly, copy, nullable) NSGradient *textViewInsideGradientActiveWindow; @property (readonly, copy, nullable) NSGradient *textViewInsideGradientInactiveWindow; @property (readonly, copy, nullable) NSColor *textViewOutsidePrimaryShadowColorActiveWindow; @property (readonly, copy, nullable) NSColor *textViewOutsidePrimaryShadowColorInactiveWindow; @property (readonly, copy, nullable) NSColor *textViewOutsideSecondaryShadowColorActiveWindow; @property (readonly, copy, nullable) NSColor *textViewOutsideSecondaryShadowColorInactiveWindow; @property (readonly, copy, nullable) NSFont *textViewFont; @property (readonly, copy, nullable) NSFont *textViewFontLarge; @property (readonly, copy, nullable) NSFont *textViewFontExtraLarge; @property (readonly, copy, nullable) NSFont *textViewFontHumongous; - (BOOL)preferredTextViewFontChanged; @property (readonly, copy, nullable) NSFont *textViewPreferredFont; @property (readonly) TVCMainWindowTextViewFontSize textViewPreferredFontSize; // not assigned until -preferredFont is called #pragma mark - #pragma mark Background View @property (readonly, copy, nullable) NSColor *backgroundViewBackgroundColor; @property (readonly, copy, nullable) NSColor *backgroundViewDividerColor; @property (readonly) CGFloat backgroundViewContentBorderPadding; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCMemberList.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCChannelUser; TEXTUAL_EXTERN NSString * const TVCMemberListDragType; @interface TVCMemberList : NSTableView @property (nonatomic, assign) BOOL isHiddenByUser; - (void)refreshDrawingForMember:(IRCChannelUser *)cellItem; - (void)refreshDrawingForRow:(NSInteger)rowIndex; - (nullable id)itemAtRow:(NSInteger)row; - (NSInteger)rowForItem:(nullable id)item; // -1 = not found @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCMemberListAppearance.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelUser.h" #import "TVCMainWindowAppearance.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMemberListAppearance : TVCApplicationAppearance @property (readonly) CGFloat defaultWidth; @property (readonly) CGFloat minimumWidth; @property (readonly) CGFloat maximumWidth; @property (readonly, copy, nullable) NSColor *rowSelectionColorActiveWindow; @property (readonly, copy, nullable) NSColor *rowSelectionColorInactiveWindow; #pragma mark - #pragma mark Member Cell @property (readonly) BOOL cellRowEmphasized; @property (readonly, copy, nullable) NSColor *cellTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *cellTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *cellAwayTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *cellAwayTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *cellSelectedTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *cellSelectedTextColorInactiveWindow; @property (readonly, copy, nullable) NSFont *cellFont; @property (readonly, copy, nullable) NSFont *cellFontSelected; #pragma mark - #pragma mark Mark Badge @property (readonly) CGFloat markBadgeLeftMargin; @property (readonly, copy, nullable) NSColor *markBadgeBackgroundColorActiveWindow; @property (readonly, copy, nullable) NSColor *markBadgeBackgroundColorInactiveWindow; @property (readonly, copy, nullable) NSColor *markBadgeSelectedBackgroundColorActiveWindow; @property (readonly, copy, nullable) NSColor *markBadgeSelectedBackgroundColorInactiveWindow; @property (readonly, copy, nullable) NSColor *markBadgeTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *markBadgeTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *markBadgeSelectedTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *markBadgeSelectedTextColorInactiveWindow; @property (readonly, copy, nullable) NSFont *markBadgeFont; @property (readonly, copy, nullable) NSFont *markBadgeFontSelected; #pragma mark - #pragma mark Mark Badge Modes /* These will never be nil because default is stored in preferences. */ @property (readonly, copy) NSColor *markBadgeBackgroundColor_Y; @property (readonly, copy) NSColor *markBadgeBackgroundColor_A; @property (readonly, copy) NSColor *markBadgeBackgroundColor_H; @property (readonly, copy) NSColor *markBadgeBackgroundColor_O; @property (readonly, copy) NSColor *markBadgeBackgroundColor_Q; @property (readonly, copy) NSColor *markBadgeBackgroundColor_V; /* -markBadgeBackgroundColorByUser is the no mode ("x") background color defined by user. This value can be nil. Use the activeWindow or inactiveWindow background colors defined above when this is nil. */ @property (readonly, copy, nullable) NSColor *markBadgeBackgroundColorByUser; #pragma mark - #pragma mark Accessors - (nullable NSImage *)cachedUserMarkBadgeForSymbol:(NSString *)modeSymbol rank:(IRCUserRank)rank; - (void)cacheUserMarkBadge:(NSImage *)badgeImage forSymbol:(NSString *)modeSymbol rank:(IRCUserRank)rank; - (void)invalidateUserMarkBadgeCacheForSymbol:(NSString *)modeSymbol rank:(IRCUserRank)rank; - (void)invalidateUserMarkBadgeCaches; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCServerList.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCTreeItem; TEXTUAL_EXTERN NSString * const TVCServerListDragType; @interface TVCServerList : NSOutlineView - (void)refreshDrawingForItem:(IRCTreeItem *)cellItem; - (void)refreshDrawingForRow:(NSInteger)rowIndex; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCServerListAppearance.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindowAppearance.h" NS_ASSUME_NONNULL_BEGIN @interface TVCServerListAppearance : TVCApplicationAppearance @property (readonly) CGFloat defaultWidth; @property (readonly) CGFloat minimumWidth; @property (readonly) CGFloat maximumWidth; @property (readonly, copy, nullable) NSColor *rowSelectionColorActiveWindow; @property (readonly, copy, nullable) NSColor *rowSelectionColorInactiveWindow; #pragma mark - #pragma mark Server Cell @property (readonly) BOOL serverRowEmphasized; @property (readonly) CGFloat serverLabelLeftMargin; @property (readonly, copy, nullable) NSColor *serverTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *serverTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *serverDisabledTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *serverDisabledTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *serverSelectedTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *serverSelectedTextColorInactiveWindow; @property (readonly, copy, nullable) NSFont *serverFont; @property (readonly, copy, nullable) NSFont *serverFontSelected; #pragma mark - #pragma mark Channel Cell @property (readonly) BOOL channelRowEmphasized; @property (readonly, copy, nullable) NSColor *channelTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *channelTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *channelDisabledTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *channelDisabledTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *channelSelectedTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *channelSelectedTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *channelErroneousTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *channelErroneousTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *channelHighlightTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *channelHighlightTextColorInactiveWindow; @property (readonly, copy, nullable) NSFont *channelFont; @property (readonly, copy, nullable) NSFont *channelFontSelected; #pragma mark - #pragma mark Message Count Badge @property (readonly, copy, nullable) NSColor *unreadBadgeBackgroundColorActiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeBackgroundColorInactiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeSelectedBackgroundColorActiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeSelectedBackgroundColorInactiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeHighlightBackgroundColorActiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeHighlightBackgroundColorInactiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeHighlightBackgroundColorByUser; @property (readonly, copy, nullable) NSColor *unreadBadgeTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeSelectedTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeSelectedTextColorInactiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeHighlightTextColorActiveWindow; @property (readonly, copy, nullable) NSColor *unreadBadgeHighlightTextColorInactiveWindow; @property (readonly, copy, nullable) NSFont *unreadBadgeFont; @property (readonly, copy, nullable) NSFont *unreadBadgeFontSelected; @property (readonly) CGFloat unreadBadgeMinimumWidth; @property (readonly) CGFloat unreadBadgeHeight; @property (readonly) CGFloat unreadBadgePadding; #pragma mark - #pragma mark Accessors - (nullable NSString *)statusIconForActiveChannel:(BOOL)isActive selected:(BOOL)isSelected activeWindow:(BOOL)isActiveWindow treatAsTemplate:(BOOL *)treatAsTemplate; - (nullable NSString *)statusIconForActiveQuery:(BOOL)isActive selected:(BOOL)isSelected activeWindow:(BOOL)isActiveWindow treatAsTemplate:(BOOL *)treatAsTemplate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCTextViewWithIRCFormatter.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOKeyEventHandler.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TVCTextViewCaretLocation) { TVCTextViewCaretLocationOnlyLine, // There isn't more than one line TVCTextViewCaretLocationFirstLine, TVCTextViewCaretLocationMiddle, TVCTextViewCaretLocationLastLine, }; @interface TVCTextViewWithIRCFormatter : NSTextView @property (readonly) TVCTextViewCaretLocation caretLocation; - (CGFloat)highestHeightBelowHeight:(CGFloat)maximumHeight withPadding:(CGFloat)valuePadding; @property (readonly) NSRect selectedRect; @property (nonatomic, copy) NSString *stringValue; @property (nonatomic, copy) NSString *stringValueWithIRCFormatting; @property (nonatomic, copy) NSAttributedString *attributedStringValue; - (void)resetFontInRange:(NSRange)range; - (void)resetFontColorInRange:(NSRange)range; - (void)resetTypeSetterAttributes; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCValidatedComboBox.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCValidatedTextField.h" NS_ASSUME_NONNULL_BEGIN @interface TVCValidatedComboBox : NSComboBox @property (nonatomic, copy, nullable) TVCValidatedTextFieldValidationBlock validationBlock; @property (nonatomic, assign) BOOL stringValueUsesOnlyFirstToken; // Only use everything before first space (" ") as value. @property (nonatomic, assign) BOOL stringValueIsTrimmed; // -stringValueUsesOnlyFirstToken returns a trimmed value of newlines and spaces. However, if you want more than first token, then specify this. @property (nonatomic, assign) BOOL stringValueIsInvalidOnEmpty; // Is an empty string considered invalid? @property (nonatomic, assign) BOOL performValidationWhenEmpty; @property (nonatomic, weak) id textDidChangeCallback; // Calls method "-(void)validatedTextFieldTextDidChange:(id)sender" whereas "sender" is the text field. @property (nonatomic, assign) BOOL caseInsensitiveComplete; // Whether completions are case insensitive. Default NO. @property (nonatomic, copy, nullable) NSString *defaultValue; // A value to return from -value if the text field is empty. Only used if stringValueIsInvalidOnEmpty = NO @property (readonly, copy) NSString *value; /* The current value. */ @property (readonly, copy) NSString *lowercaseValue; @property (readonly, copy) NSString *uppercaseValue; @property (readonly) BOOL valueIsEmpty; @property (readonly) BOOL valueIsValid; @property (readonly) BOOL valueIsPredefined; @property (readonly, copy, nullable) NSString *lastValidationErrorDescription; - (BOOL)showValidationErrorPopover; - (void)closeValidationErrorPopover; - (void)performValidation; /* Force the text field to clear cache and validate value */ @end @interface TVCValidatedComboBoxCell : NSComboBoxCell @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TVCValidatedTextField.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN /* Keep the validation block as fast as possible as it is called every time that the value of the text field is changed. */ /* Validation block returns nil on success or a string that can be presented to the user as an error description. */ typedef NSString * _Nullable (^TVCValidatedTextFieldValidationBlock)(NSString *currentValue); @interface TVCValidatedTextField : NSTextField @property (nonatomic, copy, nullable) TVCValidatedTextFieldValidationBlock validationBlock; @property (nonatomic, assign) BOOL stringValueUsesOnlyFirstToken; // Only use everything before first space (" ") as value. @property (nonatomic, assign) BOOL stringValueIsTrimmed; // Returned value is trimmed of whitespaces and newlines when returned. The value is returned trimmed by -value. It is also sent to the validation block as trimmed. @property (nonatomic, assign) BOOL stringValueIsInvalidOnEmpty; // Is an empty string considered invalid? @property (nonatomic, assign) BOOL performValidationWhenEmpty; @property (nonatomic, weak) id textDidChangeCallback; // Calls method "-(void)validatedTextFieldTextDidChange:(id)sender" whereas "sender" is the text field. @property (nonatomic, copy, nullable) NSString *defaultValue; // A value to return from -value if the text field is empty. Only used if stringValueIsInvalidOnEmpty = NO @property (readonly, copy) NSString *value; /* The current value. */ @property (readonly, copy) NSString *lowercaseValue; @property (readonly, copy) NSString *uppercaseValue; @property (readonly) BOOL valueIsEmpty; @property (readonly) BOOL valueIsValid; @property (readonly, copy, nullable) NSString *lastValidationErrorDescription; - (BOOL)showValidationErrorPopover; - (void)closeValidationErrorPopover; - (void)performValidation; /* Force the text field to clear cache and validate value */ @end @interface TVCValidatedTextFieldCell : NSTextFieldCell @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TXAppearance.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TXAppearanceType) { TXAppearanceTypeBigSurLight, TXAppearanceTypeBigSurDark, }; /* TXAppKitAppearanceTarget defines which items the NSAppearance object returned by -appKitAppearance should be assigned to. */ typedef NS_ENUM(NSUInteger, TXAppKitAppearanceTarget) { /* The NSAppearance object should be assigned to the window. */ TXAppKitAppearanceTargetWindow, /* The NSAppearance object shouldn't be assigned to anything. */ TXAppKitAppearanceTargetNone }; /* None of these properties are observable. See -[TXAppearance properties] for information about observing. */ @protocol TXAppearanceProperties @property (readonly, copy) NSString *appearanceName; @property (readonly) TXAppearanceType appearanceType; @property (readonly, copy) NSString *shortAppearanceDescription; // e.g. "light", "dark" @property (readonly) BOOL isDarkAppearance; @property (readonly) TXAppKitAppearanceTarget appKitAppearanceTarget; @property (readonly, nullable) NSAppearance *appKitAppearance; // nil when -appKitAppearanceTarget = none @end @interface TXAppearancePropertyCollection : NSObject @property (readonly, class) BOOL systemWideDarkModeEnabled; @property (readonly, class, nullable) NSAppearance *appKitLightAppearance; @property (readonly, class, nullable) NSAppearance *appKitDarkAppearance; @end /* Access through +[TXSharedApplication sharedAppearance] */ @interface TXAppearance : NSObject /* TXAppearance replaces the property collection object whenever the appearance changes so that there is no delay from when one property is set and another is set. Observe changes to the properties collection object and not an individual property. Latter will not work. */ @property (readonly, strong) TXAppearancePropertyCollection *properties; @end TEXTUAL_EXTERN NSNotificationName const TXApplicationAppearanceChangedNotification; TEXTUAL_EXTERN NSNotificationName const TXSystemAppearanceChangedNotification; NS_ASSUME_NONNULL_END #import "TXAppearanceHelper.h" ================================================ FILE: Sources/App/Classes/Headers/TXAppearanceHelper.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface NSView (TXAppearance) /* Posted when the application appearance changes */ /* The default implementation does nothing nor is super required */ - (void)applicationAppearanceChanged; /* Can return YES to change default implementation of -applicationAppearanceChanged to set needsDisplay to YES. */ @property (readonly) BOOL needsDisplayWhenApplicationAppearanceChanges; /* Returns YES by default. If NO, -applicationAppearanceChanged will not be sent beyond the view that returned NO. */ @property (readonly) BOOL sendApplicationAppearanceChangedToSubviews; /* Performs -applicationAppearanceChanged on view and all subviews if -sendApplicationAppearanceChangedToSubviews doesn't return NO. */ - (void)notifyApplicationAppearanceChanged; /* Posted when the system appearance changes */ /* The default implementation does nothing nor is super required */ - (void)systemAppearanceChanged; /* Can return YES to change default implementation of -systemAppearanceChanged to set needsDisplay to YES. */ @property (readonly) BOOL needsDisplayWhenSystemAppearanceChanges; /* Returns YES by default. If NO, -systemAppearanceChanged will not be sent beyond the view that returned NO. */ @property (readonly) BOOL sendSystemAppearanceChangedToSubviews; /* Performs -systemAppearanceChanged on view and all subviews if -sendSystemAppearanceChangedToSubviews doesn't return NO. */ - (void)notifySystemAppearanceChanged; @end @interface NSWindow (TXApplication) /* Performs -applicationAppearanceChanged on window beginning with the window frame which contains title and content view. */ - (void)notifyApplicationAppearanceChanged; /* Performs -systemAppearanceChanged on window beginning with the window frame which contains title and content view. */ - (void)notifySystemAppearanceChanged; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TXGlobalModels.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN /* Time */ TEXTUAL_EXTERN NSString * _Nullable TXFormattedTimestamp(NSDate *date, NSString *format); TEXTUAL_EXTERN NSString * _Nullable TXHumanReadableTimeInterval(NSTimeInterval dateInterval, BOOL shortValue, NSCalendarUnit orderMatrix); TEXTUAL_EXTERN NSString * _Nullable TXFormatDate(id dateObject, NSDateFormatterStyle dateStyle, NSDateFormatterStyle timeStyle, BOOL relativeOutput); TEXTUAL_EXTERN NSString * _Nullable TXFormatDateLongStyle(id dateObject, BOOL relativeOutput); /* Everything else */ TEXTUAL_EXTERN NSString *TXFormattedNumber(NSInteger number); TEXTUAL_EXTERN NSUInteger TXRandomNumber(u_int32_t maximum); TEXTUAL_EXTERN NSComparator NSDefaultComparator; NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TXMasterController.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCWorld, TVCMainWindow, TXMenuController; @interface TXMasterController : NSObject @property (readonly) BOOL debugModeIsOn; @property (readonly) BOOL ghostModeIsOn; @property (readonly) BOOL applicationIsActive; @property (readonly) BOOL applicationIsChangingActiveState; @property (readonly) BOOL applicationIsLaunched; @property (readonly) BOOL applicationIsTerminating; @property (readonly) IRCWorld *world; @property (readonly) TVCMainWindow *mainWindow; @property (readonly, weak) TXMenuController *menuController; @property (nonatomic, assign) BOOL skipTerminateSave; @end #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 @class SPUStandardUpdaterController; @interface TXMasterController (Sparkle) @property (readonly) SPUStandardUpdaterController *updateController; @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TXMenuController.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class IRCClient, IRCChannel, IRCChannelUser; /* MT = Menu Tags. Each enum holds integers for different menu items so that they can be referenced programatically. */ /* For submenu tags, we take the tag of the parent, add four zeros to the end, then start from there. */ enum { /* Main menu */ MTMainMenuApp = 1, MTMainMenuFile = 2, MTMainMenuEdit = 3, MTMainMenuView = 4, MTMainMenuServer = 5, MTMainMenuChannel = 6, MTMainMenuQuery = 7, MTMainMenuNavigate = 8, MTMainMenuWindow = 9, MTMainMenuHelp = 10, /* Main menu - App menu */ MTMMAppAboutApp = 100, // "About Textual" MTMMAppAboutAppSeparator = 101, // "-" MTMMAppPreferences = 102, // "Preferences…" MTMMAppManageLicense = 103, // "Manage license…" // MTMMAppInAppPurchase = 104, // "In-app Purchase…" MTMMAppCheckForUpdates = 105, // "Check for updates…" MTMMAppCheckForUpdatesSeparator = 106, // "-" MTMMAppServices = 107, // "Services" MTMMAppServicesSeparator = 108, // "-" MTMMAppHideApp = 109, // "Hide Textual" MTMMAppHideOthers = 110, // "Hide Others" MTMMAppShowAll = 111, // "Show All" MTMMAppShowAllSeparator = 112, // "-" MTMMAppQuitApp = 113, // "Quit Textual & IRC" /* Main menu - File menu */ MTMMFileDisableAllNotifications = 200, // "Disable All Notifications" MTMMFileDisableAllNotificationSounds = 201, // "Disable All Notification Sounds" MTMMFileDisableAllNotificationSoundsSeparator = 202, // "-" MTMMFilePrint = 203, // "Print" MTMMFilePrintSeparator = 204, // "-" MTMMFileCloseWindow = 205, // "Close Window" /* Main menu - Edit menu */ MTMMEditUndo = 300, // "Undo" MTMMEditRedo = 301, // "Redo" MTMMEditRedoSeparator = 302, // "-" MTMMEditCut = 303, // "Cut" MTMMEditCopy = 304, // "Copy" MTMMEditPaste = 305, // "Paste" MTMMEditDelete = 306, // "Delete" MTMMEditSelectAll = 307, // "Select All" MTMMEditSelectAllSeparator = 308, // "-" MTMMEditFindMenu = 309, // "Find" MTMMEditFindMenuFind = 3090000, // "Find…" MTMMEditFindMenuFindNext = 3090001, // "Find Next" MTMMEditFindMenuFindPrevious = 3090002, // "Find Previous" /* Main menu - View menu */ MTMMViewMarkScrollback = 400, // "Mark Scrollback" MTMMViewScrollbackMarker = 401, // "Scrollback Marker" MTMMViewScrollbackMarkerSeparator = 402, // "-" MTMMViewMarkAllAsRead = 403, // "Mark All as Read" MTMMViewClearScrollback = 404, // "Clear Scrollback" MTMMViewClearScrollbackSeparator = 405, // "-" MTMMViewIncreaseFontSize = 406, // "Increase Font Size" MTMMViewDecreaseFontSize = 407, // "Decrease Font Size" MTMMViewDecreaseFontSizeSeparator = 408, // "-" MTMMViewToggleFullscreen = 409, // "Toggle Fullscreen" /* Main menu - Server menu */ MTMMServerConnect = 500, // "Connect" MTMMServerConnectWithoutProxy = 501, // "Connect Without Proxy" MTMMServerDisconnect = 502, // "Disconnect" MTMMServerCancelReconnect = 503, // "Cancel Reconnect" MTMMServerCancelReconnectSeparator = 504, // "-" MTMMServerChannelList = 505, // "Channel List…" MTMMServerChangeNickname = 506, // "Change Nickname…" MTMMServerChangeNicknameSeparator = 507, // "-" MTMMServerAddServer = 508, // "Add Server…" MTMMServerDuplicateServer = 509, // "Duplicate Server" MTMMServerDeleteServer = 510, // "Delete Server…" MTMMServerDeleteServerSeparator = 511, // "-" MTMMServerAddChannel = 512, // "Add Channel…" MTMMServerAddChannelSeparator = 513, // "-" MTMMServerServerProperties = 514, // "Server Properties…" /* Main menu - Channel menu */ MTMMChannelJoinChannel = 600, // "Join Channel" MTMMChannelLeaveChannel = 601, // "Leave Channel" MTMMChannelLeaveChannelSeparator = 602, // "-" MTMMChannelAddChannel = 603, // "Add Channel…" MTMMChannelDeleteChannel = 604, // "Delete Channel" MTMMChannelDeleteChannelSeparator = 605, // "-" MTMMChannelViewLogs = 606, // "View Logs" MTMMChannelViewLogsSeparator = 607, // "-" MTMMChannelModifyTopic = 608, // "Modify Topic" MTMMChannelModesMenu = 609, // "Modes" MTMMChannelModesMenuAddModerated = 6090000, // "Moderated (+m)" MTMMChannelModesMenuRemoveModerated = 6090001, // "Unmoderated (-m)" MTMMChannelModesMenuAddInviteOnly = 6090002, // "Invite Only (+i)" MTMMChannelModesMenuRemoveInviteOnly = 6090003, // "Anyone Can Join (-i)" MTMMChannelModesMenuManageAllModes = 6090004, // "Manage All Modes" MTMMChannelModesMenuSeparator = 610, // "-" MTMMChannelListOfBans = 611, // "List of Bans" MTMMChannelListOfBanExceptions = 612, // "List of Ban Exceptions" MTMMChannelListOfInviteExceptions = 613, // "List of Invite Exceptions" MTMMChannelListOfQuiets = 614, // "List of Quiets" MTMMChannelListOfQuietsSeparator = 615, // "-" MTMMChannelChannelProperties = 616, // "Channel Properties…" MTMMChannelChannelPropertiesSeparator = 617, // "-" MTMMChannelCopyUniqueIdentifier = 618, // /* Main menu - Query menu */ MTMMQueryCloseQuery = 1800, // "Close Query" MTMMQueryCloseQuerySeparator = 1801, // "-" MTMMQueryQueryLogs = 1802, // "Query Logs" /* Main menu - Navigation menu */ MTMMNavigationServersMenu = 700, // "Servers" MTMMNavigationServersMenuNextServer = 7000000, // "Next Server" MTMMNavigationServersMenuPreviousServer = 7000001, // "Previous Server" MTMMNavigationServersMenuPreviousServerSeparator = 7000002, // "-" MTMMNavigationServersMenuNextActiveServer = 7000003, // "Next Active Server" MTMMNavigationServersMenuPreviousActiveServer = 7000004, // "Previous Active Server" MTMMNavigationChannelsMenu = 701, // "Channels" MTMMNavigationChannelsMenuNextChannel = 7010000, // "Next Channel" MTMMNavigationChannelsMenuPreviousChannel = 7010001, // "Previous Channel" MTMMNavigationChannelsMenuPreviousChannelSeparator = 7010002, // "-" MTMMNavigationChannelsMenuNextActiveChannel = 7010003, // "Next Active Channel" MTMMNavigationChannelsMenuPreviousActiveChannel = 7010004, // "Previous Active Channel" MTMMNavigationChannelsMenuPreviousActiveChannelSeparator = 7010005, // "-" MTMMNavigationChannelsMenuNextUnreadChannel = 7010006, // "Next Unread Channel" MTMMNavigationChannelsMenuPreviousUnreadChannel = 7010007, // "Previous Unread Channel" MTMMNavigationChannelsMenuSeparator = 702, // "-" MTMMNavigationMoveBackward = 703, // "Move Backward" MTMMNavigationMoveForward = 704, // "Move Forward" MTMMNavigationMoveForwardSeparator = 705, // "-" MTMMNavigationPreviousSelection = 706, // "Previous Selection" MTMMNavigationPreviousSelectionSeparator = 707, // "-" MTMMNavigationNextHighlight = 708, // "Next Highlight" MTMMNavigationPreviousHighlight = 709, // "Previous Highlight" MTMMNavigationPreviousHighlightSeparator = 710, // "-" MTMMNavigationJumpToCurrentSession = 711, // "Jump to Current Session" MTMMNavigationJumpToPresent = 712, // "Jump to Present" MTMMNavigationJumpToPresentSeparator = 713, // "-" MTMMNavigationChannelList = 714, // "Channel List…" MTMMNavigationChannelListSeparator = 715, // "-" MTMMNavigationSearchChannels = 716, // "Search channels…" /* Main menu - Window menu */ MTMMWindowMinimize = 800, // "Minimize" MTMMWindowZoom = 801, // "Zoom" MTMMWindowZoomSeparator = 802, // "-" MTMMWindowToggleVisibilityOfMemberList = 803, // "Toggle Visibility of Member List" MTMMWindowToggleVisibilityOfServerList = 804, // "Toggle Visibility of Server List" MTMMWindowToggleWindowAppearance = 805, // "Toggle Window Appearance" MTMMWindowToggleWindowAppearanceSeparator = 806, // "-" MTMMWindowSortChannelList = 807, // "Sort Channel List" MTMMWindowSortChannelListSeparator = 808, // "-" MTMMWindowCenterWindow = 809, // "Center Window" MTMMWindowResetWindowToDefaultSize = 810, // "Reset Window to Default Size" MTMMWindowResetWindowToDefaultSizeSeparator = 811, // "-" MTMMWindowMainWindow = 812, // "Main Window" MTMMWindowAddressBook = 813, // "Address Book" MTMMWindowIgnoreList = 814, // "Ignore List" MTMMWindowViewLogs = 815, // "View Logs" MTMMWindowHighlightList = 816, // "Highlight List" MTMMWindowFileTransfers = 817, // "File Transfers" MTMMWindowFileTransfersSeparator = 818, // "-" MTMMWindowBrightAllToFront = 819, // "Bring All to Front" /* Main menu - Help menu */ MTMMHelpAcknowledgements = 900, // "Acknowledgements" MTMMHelpLicenseAgreement = 901, // "License Agreement" MTMMHelpPrivacyPolicy = 902, // "Privacy Policy" MTMMHelpPrivacyPolicySeparator = 903, // "-" MTMMHelpFrequentlyAskedQuestions = 904, // "Frequently Asked Questions" MTMMHelpKnowledgeBaseMenu = 905, // "Knowledge Base" /* Highest: 9050016 */ MTMMHelpKBMenuKnowledgeBaseHome = 9050000, // "Knowledge Base Home" MTMMHelpKBMenuKnowledgeBaseHomeSeparator = 9050001, // "-" MTMMHelpKBMenuChatEncryption = 9050004, // "Chat Encryption" MTMMHelpKBMenuCommandReference = 9050005, // "Command Reference" MTMMHelpKBMenuFeatureRequests = 9050006, // "Feature Requests" MTMMHelpKBMenuKeyboardShortcuts = 9050007, // "Keyboard Shortcuts" MTMMHelpKBMenuMemoryManagement = 9050008, // "Memory Management" MTMMHelpKBMenuNetworkTimeouts = 9050016, // "Network Timeouts" MTMMHelpKBMenuTextFormatting = 9050009, // "Text Formatting" MTMMHelpKBMenuStylingInformation = 9050010, // "Styling Information" MTMMHelpKBMenuStylingInformationSeparator = 9050011, // "-" MTMMHelpKBMenuConnectingWithCertificate = 9050012, // "Connecting with Certificate" MTMMHelpKBMenuConnectingToBouncer = 9050013, // "Connecting to a ZNC Bouncer" MTMMHelpKBMenuConnectingToBouncerSeparator = 9050014, // "-" MTMMHelpKBMenuDCCFileTransferInformation = 9050015, // "DCC File Transfer Information" MTMMHelpKnowledgeBaseMenuSeparator = 906, // "-" MTMMHelpConnectToHelpChannel = 907, // "Connect to Help Channel" MTMMHelpConnectToTestingChannel = 908, // "Connect to Testing Channel" MTMMHelpConnectToTestingChannelSeparator = 909, // "-" MTMMHelpAdvancedMenu = 910, // "Advanced" MTMMHelpAdvancedMenuEnableDeveloperMode = 9100000, // "Enable Developer Mode" MTMMHelpAdvancedMenuEnableDeveloperModeSeparator = 9100001, // "-" MTMMHelpAdvancedMenuHiddenPreferences = 9100002, // "Hidden Preferences…" MTMMHelpAdvancedMenuHiddenPreferencesSeparator = 9100003, // "-" MTMMHelpAdvancedMenuExportPreferences = 9100004, // "Export Preferences" MTMMHelpAdvancedMenuImportPreferences = 9100005, // "Import Preferences" MTMMHelpAdvancedMenuImportPreferencesSeparator = 9100006, // "-" MTMMHelpAdvancedMenuResetDontAskMeWarnings = 9100007, // "Reset 'Don't Ask Me' Warnings" /* WebKit channel name menu */ MTWKChannelNameJoinChannel = 1000, // "Join Channel" /* WebKit URL menu */ MTWKURLCopyURL = 1100, // "Copy URL" /* WebKit general menu */ MTWKGeneralChangeNickname = 1200, // "Change Nickname…" MTWKGeneralChangeNicknameSeparator = 1201, // "-" MTWKGeneralSearchWithGoogle = 1202, // "Search With Google" MTWKGeneralLookUpInDictionary = 1203, // "Look Up In Dictionary" MTWKGeneralLookUpInDictionarySeparator = 1204, // "-" MTWKGeneralCopy = 1205, // "Copy" MTWKGeneralPaste = 1206, // "Paste" MTWKGeneralPasteSeparator = 1207, // "-" MTWKGeneralQueryLogs = 1208, // "Query Logs" MTWKGeneralChannelMenu = 1209, // "Channel" /* Main window segmented controller */ MTMainWindowSegmentedControllerAddServer = 1300, // "Add Server…" MTMainWindowSegmentedControllerAddServerSeparator = 1301, // "-" MTMainWindowSegmentedControllerAddChannel = 1302, // "Add Channel…" /* Empty server list menu */ MTMainWindowServerListAddServer = 1400, // "Add Server…" /* Off-the-Record Messaging status button */ MTOTRStatusButtonWhatIsThis = 1500, // "What is this?" MTOTRStatusButtonWhatIsThisSeparator = 1501, // "-" MTOTRStatusButtonStartPrivateConversation = 1502, // "Start Private Conversation" MTOTRStatusButtonRefreshPrivateConversation = 1503, // "Refresh Private Conversation" MTOTRStatusButtonEndPrivateConversation = 1504, // "End Private Conversation" MTOTRStatusButtonEndPrivateConversationSeparator = 1505, // "-" MTOTRStatusButtonAuthenticateChatPartner = 1506, // "Authenticate Chat Partner" MTOTRStatusButtonAuthenticateChatPartnerSeparator = 1507, // "-" MTOTRStatusButtonViewListOfFingerprints = 1508, // "View List of Fingerprints" /* User context menu */ MTUserControlsLowestTag = 1600, MTUserControlsHighestTag = 1699, MTUserControlsAddIgnore = 1600, // "Add Ignore" MTUserControlsModifyIgnore = 1601, // "Modify Ignore" MTUserControlsRemoveIgnore = 1602, // "Remove Ignore" MTUserControlsRemoveIgnoreSeparator = 1603, // "-" MTUserControlsInviteTo = 1604, // "Invite to…" MTUserControlsInviteToSeparator = 1605, // "-" MTUserControlsGetInfo = 1606, // "Get Info (Whois)" MTUserControlsPrivateMessage = 1607, // "Private Message (Query)" MTUserControlsPrivateMessageSeparator = 1608, // "-" MTUserControlsGiveOp = 1609, // "Give Op (+o)" MTUserControlsGiveHalfop = 1610, // "Give Halfop (+h)" MTUserControlsGiveVoice = 1611, // "Give Voice (+v)" MTUserControlsAllModesGiven = 1612, // "All Modes Given" MTUserControlsAllModesGivenSeparator = 1613, // "-" MTUserControlsTakeOp = 1614, // "Take Op (-o)" MTUserControlsTakeHalfop = 1615, // "Take Halfop (-h)" MTUserControlsTakeVoice = 1616, // "Take Voice (-v)" MTUserControlsAllModesTaken = 1617, // "All Modes Taken" MTUserControlsAllModesTakenSeparator = 1618, // "-" MTUserControlsBan = 1619, // "Ban" MTUserControlsKick = 1620, // "Kick" MTUserControlsBanAndKick = 1621, // "Ban and Kick" MTUserControlsBanAndKickSeparator = 1622, // "-" MTUserControlsClientToClientMenu = 1623, // "Client-to-Client" MTUserControlsClientToClientMenuSendFile = 16230000, // "Send file…" MTUserControlsClientToClientMenuSendFileSeparator = 16230001, // "-" MTUserControlsClientToClientMenuLag = 16230002, // "Lag (PING)" MTUserControlsClientToClientMenuLocalTime = 16230003, // "Local Time (TIME)" MTUserControlsClientToClientMenuLocalTimeSeparator = 16230004, // "-" MTUserControlsClientToClientMenuClientInformation = 16230005, // "Client Information (CLIENTINFO)" MTUserControlsClientToClientMenuClientVersion = 16230006, // "Client Version (VERSION)" MTUserControlsClientToClientMenuClientVersionSeparator = 16230007, // "-" MTUserControlsClientToClientMenuUserInformationFinger = 16230008, // "User Information (FINGER)" MTUserControlsClientToClientMenuUserInformationUserinfo = 16230009, // "User Information (USERINFO)" MTUserControlsIRCOperatorMenu = 1624, // "IRC Operator" MTUserControlsIRCOperatorMenuSetVirtualHost = 16240000, // "Set Virtual Host (vHost)" MTUserControlsIRCOperatorMenuSetVirtualHostSeparator = 16240001, // "-" MTUserControlsIRCOperatorMenuKillFromServer = 16240002, // "Kill from Server" MTUserControlsIRCOperatorMenuShunOnServer = 16240003, // "Shun on Server" MTUserControlsIRCOperatorMenuBanFromServer = 16240004, // "Ban from Server (G:Line)" /* Dock menu */ MTDockMenuDisableAllNotifications = 1700, // "Disable All Notifications" MTDockMenuDisableAllNotificationSounds = 1701, // "Disable All Notification Sounds" }; @interface TXMenuController : NSObject @property (readonly, strong) NSMenu *channelViewChannelNameMenu; @property (readonly, strong) NSMenu *channelViewGeneralMenu; @property (readonly, strong) NSMenu *channelViewURLMenu; @property (readonly, strong) NSMenu *dockMenu; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 @property (readonly,strong) NSMenu *encryptionManagerStatusMenu; #endif @property (readonly, weak) NSMenu *mainMenuNavigationChannelListMenu; @property (readonly, weak) NSMenu *mainMenuChannelMenu; @property (readonly, weak) NSMenu *mainMenuQueryMenu; @property (readonly, weak) NSMenuItem *mainMenuChannelMenuItem; @property (readonly, weak) NSMenuItem *mainMenuQueryMenuItem; @property (readonly, weak) NSMenuItem *mainMenuServerMenuItem; @property (readonly, strong) NSMenu *mainWindowSegmentedControllerCellMenu; @property (readonly, strong) NSMenu *serverListNoSelectionMenu; @property (readonly, strong) NSMenu *userControlMenu; @property (readonly, weak) IRCClient *selectedClient; @property (readonly, weak) IRCChannel *selectedChannel; - (NSArray *)selectedMembers:(id)sender; - (NSArray *)selectedMembersNicknames:(id)sender; - (void)deselectMembers:(id)sender; - (IBAction)copy:(id)sender; - (IBAction)paste:(id)sender; - (IBAction)print:(id)sender; - (IBAction)closeWindow:(id)sender; - (IBAction)contactSupport:(id)sender; - (IBAction)addChannel:(id)sender; - (IBAction)deleteChannel:(id)sender; - (IBAction)addServer:(id)sender; - (IBAction)duplicateServer:(id)sender; - (IBAction)deleteServer:(id)sender; - (IBAction)joinChannel:(id)sender; - (IBAction)leaveChannel:(id)sender; - (IBAction)connect:(id)sender; - (IBAction)connectBypassingProxy:(id)sender; - (IBAction)connectToTextualHelpChannel:(id)sender; - (IBAction)connectToTextualTestingChannel:(id)sender; - (IBAction)disconnect:(id)sender; - (IBAction)cancelReconnection:(id)sender; - (IBAction)clearScrollback:(id)sender; - (IBAction)markAllAsRead:(id)sender; - (IBAction)decreaseLogFontSize:(id)sender; - (IBAction)increaseLogFontSize:(id)sender; - (IBAction)jumpToCurrentSession:(id)sender; - (IBAction)jumpToPresent:(id)sender; - (IBAction)gotoScrollbackMarker:(id)sender; - (IBAction)markScrollback:(id)sender; - (IBAction)exportPreferences:(id)sender; - (IBAction)importPreferences:(id)sender; - (IBAction)memberAddIgnore:(id)sender; - (IBAction)memberModifyIgnore:(id)sender; - (IBAction)memberRemoveIgnore:(id)sender; - (IBAction)memberBanFromChannel:(id)sender; - (IBAction)memberKickFromChannel:(id)sender; - (IBAction)memberKickbanFromChannel:(id)sender; - (IBAction)memberModeGiveHalfop:(id)sender; - (IBAction)memberModeGiveOp:(id)sender; - (IBAction)memberModeGiveVoice:(id)sender; - (IBAction)memberModeTakeHalfop:(id)sender; - (IBAction)memberModeTakeOp:(id)sender; - (IBAction)memberModeTakeVoice:(id)sender; - (IBAction)memberSendCTCPClientInfo:(id)sender; - (IBAction)memberSendCTCPFinger:(id)sender; - (IBAction)memberSendCTCPPing:(id)sender; - (IBAction)memberSendCTCPTime:(id)sender; - (IBAction)memberSendCTCPUserinfo:(id)sender; - (IBAction)memberSendCTCPVersion:(id)sender; - (IBAction)memberSendFileRequest:(id)sender; - (IBAction)memberSendInvite:(id)sender; - (IBAction)memberSendWhois:(id)sender; - (IBAction)memberBanFromServer:(id)sender; - (IBAction)memberKillFromServer:(id)sender; - (IBAction)memberShunOnServer:(id)sender; - (IBAction)memberStartPrivateMessage:(id)sender; - (IBAction)onNextHighlight:(id)sender; - (IBAction)onPreviousHighlight:(id)sender; - (IBAction)openStandaloneStoreWebpage:(id)sender;; - (IBAction)openChannelLogs:(id)sender; - (IBAction)openLogLocation:(id)sender; - (IBAction)centerMainWindow:(id)sender; - (IBAction)resetMainWindowFrame:(id)sender; - (IBAction)openAcknowledgements:(id)sender; - (IBAction)showAboutWindow:(id)sender; - (IBAction)showAddressBook:(id)sender; - (IBAction)showChannelBanExceptionList:(id)sender; - (IBAction)showChannelBanList:(id)sender; - (IBAction)showChannelInviteExceptionList:(id)sender; - (IBAction)showChannelQuietList:(id)sender; - (IBAction)showChannelModifyModesSheet:(id)sender; - (IBAction)showChannelModifyTopicSheet:(id)sender; - (IBAction)showChannelPropertiesSheet:(id)sender; - (IBAction)showChannelSpotlightWindow:(id)sender; - (IBAction)showFileTransfersWindow:(id)sender; - (IBAction)showFindPrompt:(id)sender; - (IBAction)showHiddenPreferences:(id)sender; - (IBAction)showIgnoreList:(id)sender; - (IBAction)showMainWindow:(id)sender; - (IBAction)showNotificationPreferences:(id)sender; - (IBAction)showPreferencesWindow:(id)sender; - (IBAction)showServerChangeNicknameSheet:(id)sender; - (IBAction)showServerChannelList:(id)sender; - (IBAction)showServerHighlightList:(id)sender; - (IBAction)showServerPropertiesSheet:(id)sender; - (IBAction)showSetVhostPrompt:(id)sender; - (IBAction)showStylePreferences:(id)sender; - (IBAction)showWelcomeSheet:(id)sender; - (IBAction)sortChannelListNames:(id)sender; - (IBAction)toggleChannelInviteMode:(id)sender; - (IBAction)toggleChannelModerationMode:(id)sender; - (IBAction)toggleFullscreen:(id)sender; - (IBAction)toggleMainWindowAppearance:(id)sender; - (IBAction)resetMainWindowAppearance:(id)sender; - (IBAction)toggleDeveloperMode:(id)sender; - (IBAction)toggleServerListVisibility:(id)sender; - (IBAction)toggleMemberListVisibility:(id)sender; - (IBAction)toggleMuteOnNotifications:(id)sender; - (IBAction)toggleMuteOnNotificationSounds:(id)sender; - (IBAction)manageLicense:(id)sender; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (IBAction)encryptionWhatIsThisInformation:(id)sender; - (IBAction)encryptionStartPrivateConversation:(id)sender; - (IBAction)encryptionRefreshPrivateConversation:(id)sender; - (IBAction)encryptionEndPrivateConversation:(id)sender; - (IBAction)encryptionAuthenticateChatPartner:(id)sender; - (IBAction)encryptionListFingerprints:(id)sender; #endif - (IBAction)copyUniqueIdentifier:(id)sender; - (IBAction)copyUrl:(id)sender; - (IBAction)lookUpInDictionary:(id)sender; - (IBAction)searchGoogle:(id)sender; - (IBAction)copyLogAsHtml:(id)sender; - (IBAction)forceReloadTheme:(id)sender; - (IBAction)openWebInspector:(id)sender; - (IBAction)checkForUpdates:(id)sender; - (IBAction)resetDoNotAskMePopupWarnings:(id)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/TXSharedApplication.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TXAppearance, TXMasterController, TPCThemeController; #define masterController() [self masterController] #define menuController() [masterController() menuController] #define worldController() [masterController() world] #define mainWindow() [masterController() mainWindow] #define mainWindowLoadingScreen() [mainWindow() loadingScreen] #define mainWindowServerList() [mainWindow() serverList] #define mainWindowMemberList() [mainWindow() memberList] #define mainWindowTextField() [mainWindow() inputTextField] #define themeController() [TXSharedApplication sharedThemeController] #define theme() [themeController() theme] #define themeSettings() [themeController() settings] TEXTUAL_EXTERN NSErrorDomain const TXErrorDomain; @interface TXSharedApplication : NSObject + (TXAppearance *)sharedAppearance; + (TPCThemeController *)sharedThemeController; @end @interface NSObject (TXSharedApplicationObjectExtension) - (TXMasterController *)masterController; + (TXMasterController *)masterController; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Headers/Textual.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #ifdef __OBJC__ /* Cocoa Extensions imports Cocoa for us */ #import /* Static Definitions */ #import "StaticDefinitions.h" /* IRC Controllers — Core */ #import "IRC.h" #import "IRCAddressBook.h" #import "IRCAddressBookUserTracking.h" #import "IRCChannel.h" #import "IRCChannelConfig.h" #import "IRCChannelMode.h" #import "IRCChannelUser.h" #import "IRCClient.h" #import "IRCClientConfig.h" #import "IRCColorFormat.h" #import "IRCCommandIndex.h" #import "IRCConnection.h" #import "IRCConnectionConfig.h" #import "IRCConnectionErrors.h" #import "IRCHighlightLogEntry.h" #import "IRCHighlightMatchCondition.h" #import "IRCISupportInfo.h" #import "IRCMessage.h" #import "IRCModeInfo.h" #import "IRCNetworkList.h" #import "IRCPrefix.h" #import "IRCSendingMessage.h" #import "IRCServer.h" #import "IRCTreeItem.h" #import "IRCUser.h" #import "IRCUserRelations.h" #import "IRCWorld.h" /* Framework Extensions (Helpers) */ #import "NSColorHelper.h" #import "NSStringHelper.h" #import "NSViewHelper.h" /* Dialogs */ #import "TDCAlert.h" #import "TDCInputPrompt.h" #import "TDCSheetBase.h" #import "TDCWindowBase.h" /* Helpers */ #import "THOPluginProtocol.h" #import "THOUnicodeHelper.h" /* Library */ #import "TLOEncryptionManager.h" #import "TLONotificationController.h" #import "TLOInternetAddressLookup.h" #import "TLOKeyEventHandler.h" #import "TLOLinkParser.h" #import "TLOLocalization.h" #import "TLOSoundPlayer.h" #import "TLOTimer.h" #import "TLOpenLink.h" /* Preferences */ #import "TPCApplicationInfo.h" #import "TPCPathInfo.h" #import "TPCPreferencesImportExport.h" #import "TPCPreferencesLocal.h" #import "TPCPreferencesReload.h" #import "TPCPreferencesUserDefaultsLocal.h" #import "TPCResourceManager.h" #import "TPCThemeController.h" #import "TPCTheme.h" /* View Controllers */ #import "TVCAlert.h" #import "TVCAppearance.h" #import "TVCAutoExpandingTextField.h" #import "TVCAutoExpandingTokenField.h" #import "TVCBasicTableView.h" #import "TVCChannelSelectionViewController.h" #import "TVCLogController.h" #import "TVCLogLine.h" #import "TVCLogRenderer.h" #import "TVCLogView.h" #import "TVCMainWindow.h" #import "TVCMainWindowLoadingScreen.h" #import "TVCMainWindowSplitView.h" #import "TVCMainWindowTextView.h" #import "TVCMemberList.h" #import "TVCServerList.h" #import "TVCValidatedComboBox.h" #import "TVCValidatedTextField.h" #import "TVCTextViewWithIRCFormatter.h" /* Master Controllers — Root */ #import "TXAppearance.h" #import "TXGlobalModels.h" #import "TXMasterController.h" #import "TXMenuController.h" #import "TXSharedApplication.h" #endif /* @end */ ================================================ FILE: Sources/App/Classes/Headers/TextualApplication.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #warning "TextualApplication.h" is old header file. Please use "Textual.h" #import "Textual.h" ================================================ FILE: Sources/App/Classes/Helpers/Cocoa (Objective-C)/NSColorHelper.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #define TXCalibratedRGBColor(r, g, b) ([NSColor calibratedColorWithRed:r green:g blue:b alpha:1.0]) @implementation NSColor (TXColorHelper) #pragma mark - #pragma mark IRC Text Formatting Color Codes + (NSArray *)formatterColors { static NSArray *colors = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ colors = @[ /* 0 */ [NSColor formatterWhiteColor], /* 1 */ [NSColor formatterBlackColor], /* 2 */ [NSColor formatterNavyBlueColor], /* 3 */ [NSColor formatterDarkGreenColor], /* 4 */ [NSColor formatterRedColor], /* 5 */ [NSColor formatterBrownColor], /* 6 */ [NSColor formatterPurpleColor], /* 7 */ [NSColor formatterOrangeColor], /* 8 */ [NSColor formatterYellowColor], /* 9 */ [NSColor formatterLimeGreenColor], /* 10 */ [NSColor formatterTealColor], /* 11 */ [NSColor formatterAquaCyanColor], /* 12 */ [NSColor formatterLightBlueColor], /* 13 */ [NSColor formatterFuchsiaPinkColor], /* 14 */ [NSColor formatterNormalGrayColor], /* 15 */ [NSColor formatterLightGrayColor], /* 16 */ [NSColor colorWithHexadecimalValue:@"#470000"], /* 17 */ [NSColor colorWithHexadecimalValue:@"#472100"], /* 18 */ [NSColor colorWithHexadecimalValue:@"#474700"], /* 19 */ [NSColor colorWithHexadecimalValue:@"#324700"], /* 20 */ [NSColor colorWithHexadecimalValue:@"#004700"], /* 21 */ [NSColor colorWithHexadecimalValue:@"#00472c"], /* 22 */ [NSColor colorWithHexadecimalValue:@"#004747"], /* 23 */ [NSColor colorWithHexadecimalValue:@"#002747"], /* 24 */ [NSColor colorWithHexadecimalValue:@"#000047"], /* 25 */ [NSColor colorWithHexadecimalValue:@"#2e0047"], /* 26 */ [NSColor colorWithHexadecimalValue:@"#470047"], /* 27 */ [NSColor colorWithHexadecimalValue:@"#47002a"], /* 28 */ [NSColor colorWithHexadecimalValue:@"#740000"], /* 29 */ [NSColor colorWithHexadecimalValue:@"#743a00"], /* 30 */ [NSColor colorWithHexadecimalValue:@"#747400"], /* 31 */ [NSColor colorWithHexadecimalValue:@"#517400"], /* 32 */ [NSColor colorWithHexadecimalValue:@"#007400"], /* 33 */ [NSColor colorWithHexadecimalValue:@"#007449"], /* 34 */ [NSColor colorWithHexadecimalValue:@"#007474"], /* 35 */ [NSColor colorWithHexadecimalValue:@"#004074"], /* 36 */ [NSColor colorWithHexadecimalValue:@"#000074"], /* 37 */ [NSColor colorWithHexadecimalValue:@"#4b0074"], /* 38 */ [NSColor colorWithHexadecimalValue:@"#740074"], /* 39 */ [NSColor colorWithHexadecimalValue:@"#740045"], /* 40 */ [NSColor colorWithHexadecimalValue:@"#b50000"], /* 41 */ [NSColor colorWithHexadecimalValue:@"#b56300"], /* 42 */ [NSColor colorWithHexadecimalValue:@"#b5b500"], /* 43 */ [NSColor colorWithHexadecimalValue:@"#7db500"], /* 44 */ [NSColor colorWithHexadecimalValue:@"#00b500"], /* 45 */ [NSColor colorWithHexadecimalValue:@"#00b571"], /* 46 */ [NSColor colorWithHexadecimalValue:@"#00b5b5"], /* 47 */ [NSColor colorWithHexadecimalValue:@"#0063b5"], /* 48 */ [NSColor colorWithHexadecimalValue:@"#0000b5"], /* 49 */ [NSColor colorWithHexadecimalValue:@"#7500b5"], /* 50 */ [NSColor colorWithHexadecimalValue:@"#b500b5"], /* 51 */ [NSColor colorWithHexadecimalValue:@"#b5006b"], /* 52 */ [NSColor colorWithHexadecimalValue:@"#ff0000"], /* 53 */ [NSColor colorWithHexadecimalValue:@"#ff8c00"], /* 54 */ [NSColor colorWithHexadecimalValue:@"#ffff00"], /* 55 */ [NSColor colorWithHexadecimalValue:@"#b2ff00"], /* 56 */ [NSColor colorWithHexadecimalValue:@"#00ff00"], /* 57 */ [NSColor colorWithHexadecimalValue:@"#00ffa0"], /* 58 */ [NSColor colorWithHexadecimalValue:@"#00ffff"], /* 59 */ [NSColor colorWithHexadecimalValue:@"#008cff"], /* 60 */ [NSColor colorWithHexadecimalValue:@"#0000ff"], /* 61 */ [NSColor colorWithHexadecimalValue:@"#a500ff"], /* 62 */ [NSColor colorWithHexadecimalValue:@"#ff00ff"], /* 63 */ [NSColor colorWithHexadecimalValue:@"#ff0098"], /* 64 */ [NSColor colorWithHexadecimalValue:@"#ff5959"], /* 65 */ [NSColor colorWithHexadecimalValue:@"#ffb459"], /* 66 */ [NSColor colorWithHexadecimalValue:@"#ffff71"], /* 67 */ [NSColor colorWithHexadecimalValue:@"#cfff60"], /* 68 */ [NSColor colorWithHexadecimalValue:@"#6fff6f"], /* 69 */ [NSColor colorWithHexadecimalValue:@"#65ffc9"], /* 70 */ [NSColor colorWithHexadecimalValue:@"#6dffff"], /* 71 */ [NSColor colorWithHexadecimalValue:@"#59b4ff"], /* 72 */ [NSColor colorWithHexadecimalValue:@"#5959ff"], /* 73 */ [NSColor colorWithHexadecimalValue:@"#c459ff"], /* 74 */ [NSColor colorWithHexadecimalValue:@"#ff66ff"], /* 75 */ [NSColor colorWithHexadecimalValue:@"#ff59bc"], /* 76 */ [NSColor colorWithHexadecimalValue:@"#ff9c9c"], /* 77 */ [NSColor colorWithHexadecimalValue:@"#ffd39c"], /* 78 */ [NSColor colorWithHexadecimalValue:@"#ffff9c"], /* 79 */ [NSColor colorWithHexadecimalValue:@"#e2ff9c"], /* 80 */ [NSColor colorWithHexadecimalValue:@"#9cff9c"], /* 81 */ [NSColor colorWithHexadecimalValue:@"#9cffdb"], /* 82 */ [NSColor colorWithHexadecimalValue:@"#9cffff"], /* 83 */ [NSColor colorWithHexadecimalValue:@"#9cd3ff"], /* 84 */ [NSColor colorWithHexadecimalValue:@"#9c9cff"], /* 85 */ [NSColor colorWithHexadecimalValue:@"#dc9cff"], /* 86 */ [NSColor colorWithHexadecimalValue:@"#ff9cff"], /* 87 */ [NSColor colorWithHexadecimalValue:@"#ff94d3"], /* 88 */ [NSColor colorWithHexadecimalValue:@"#000000"], /* 89 */ [NSColor colorWithHexadecimalValue:@"#131313"], /* 90 */ [NSColor colorWithHexadecimalValue:@"#282828"], /* 91 */ [NSColor colorWithHexadecimalValue:@"#363636"], /* 92 */ [NSColor colorWithHexadecimalValue:@"#4d4d4d"], /* 93 */ [NSColor colorWithHexadecimalValue:@"#656565"], /* 94 */ [NSColor colorWithHexadecimalValue:@"#818181"], /* 95 */ [NSColor colorWithHexadecimalValue:@"#9f9f9f"], /* 96 */ [NSColor colorWithHexadecimalValue:@"#bcbcbc"], /* 97 */ [NSColor colorWithHexadecimalValue:@"#e2e2e2"], /* 98 */ [NSColor colorWithHexadecimalValue:@"#ffffff"], ]; }); return colors; } + (NSColor *)formatterWhiteColor { return TXCalibratedRGBColor(1.00, 1.00, 1.00); } + (NSColor *)formatterBlackColor { return TXCalibratedRGBColor(0.00, 0.00, 0.00); } + (NSColor *)formatterNavyBlueColor { return TXCalibratedRGBColor(0.04, 0.00, 0.52); } + (NSColor *)formatterDarkGreenColor { return TXCalibratedRGBColor(0.00, 0.54, 0.08); } + (NSColor *)formatterRedColor { return TXCalibratedRGBColor(1.00, 0.05, 0.04); } + (NSColor *)formatterBrownColor { return TXCalibratedRGBColor(0.55, 0.02, 0.02); } + (NSColor *)formatterPurpleColor { return TXCalibratedRGBColor(0.55, 0.00, 0.53); } + (NSColor *)formatterOrangeColor { return TXCalibratedRGBColor(1.00, 0.54, 0.09); } + (NSColor *)formatterYellowColor { return TXCalibratedRGBColor(1.00, 1.00, 0.15); } + (NSColor *)formatterLimeGreenColor { return TXCalibratedRGBColor(0.00, 1.00, 0.15); } + (NSColor *)formatterTealColor { return TXCalibratedRGBColor(0.00, 0.53, 0.53); } + (NSColor *)formatterAquaCyanColor { return TXCalibratedRGBColor(0.00, 1.00, 1.00); } + (NSColor *)formatterLightBlueColor { return TXCalibratedRGBColor(0.07, 0.00, 0.98); } + (NSColor *)formatterFuchsiaPinkColor { return TXCalibratedRGBColor(1.00, 0.00, 0.98); } + (NSColor *)formatterNormalGrayColor { return TXCalibratedRGBColor(0.53, 0.53, 0.53); } + (NSColor *)formatterLightGrayColor { return TXCalibratedRGBColor(0.80, 0.80, 0.80); } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Helpers/Cocoa (Objective-C)/NSStringHelper.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRC.h" #import "IRCClient.h" #import "IRCISupportInfo.h" #import "IRCColorFormat.h" #import "TPCPreferencesLocal.h" #import "TVCLogRenderer.h" NS_ASSUME_NONNULL_BEGIN NSStringEncoding const TXDefaultPrimaryStringEncoding = NSUTF8StringEncoding; NSStringEncoding const TXDefaultFallbackStringEncoding = NSISOLatin1StringEncoding; @implementation NSString (TXStringHelper) - (BOOL)isValidInternetAddress { if (self.length == 0) { return NO; } if (self.isIPAddress || [self isEqualToString:@"localhost"]) { return YES; } return [self onlyContainsCharactersFromCharacterSet: [NSCharacterSet Ato9UnderscoreDashPeriod]]; } - (BOOL)isValidInternetPort { if (self.isNumericOnly == NO) { return NO; } NSInteger selfInt = self.integerValue; return (selfInt > 0 && selfInt <= TXMaximumTCPPort); } - (NSString *)stringByAppendingIRCFormattingStop { return [self stringByAppendingFormat:@"%C", IRCTextFormatterTerminatingCharacter]; } - (BOOL)hostmaskComponents:(NSString * _Nullable * _Nullable)nickname username:(NSString * _Nullable * _Nullable)username address:(NSString * _Nullable * _Nullable)address { return [self hostmaskComponents:nickname username:username address:address onClient:nil]; } - (BOOL)hostmaskComponents:(NSString * _Nullable * _Nullable)nickname username:(NSString * _Nullable * _Nullable)username address:(NSString * _Nullable * _Nullable)address onClient:(nullable IRCClient *)client { if (self.length == 0) { return NO; } /* Find first ! starting from left side of string */ NSRange bang1pos = [self rangeOfString:@"!" options:NSLiteralSearch]; /* Find first @ starting from the right side of string */ NSRange bang2pos = [self rangeOfString:@"@" options:(NSLiteralSearch | NSBackwardsSearch)]; if ((bang1pos.location == NSNotFound) || (bang2pos.location == NSNotFound) || (bang2pos.location <= bang1pos.location)) { return NO; } NSString *nicknameInt = [self substringToIndex:bang1pos.location]; NSString *usernameInt = [self substringWithRange: NSMakeRange((bang1pos.location + 1), (bang2pos.location - (bang1pos.location + 1)))]; NSString *addressInt = [self substringAfterIndex:bang2pos.location]; if ([nicknameInt isHostmaskNicknameOn:client] == NO || [usernameInt isHostmaskUsernameOn:client] == NO || [addressInt isHostmaskAddressOn:client] == NO) { return NO; } if ( nickname) { *nickname = nicknameInt; } if ( username) { *username = usernameInt; } if ( address) { *address = addressInt; } return YES; } - (BOOL)isHostmask { return [self hostmaskComponents:nil username:nil address:nil]; } - (BOOL)isHostmaskAddress { return [self isHostmaskAddressOn:nil]; } - (BOOL)isHostmaskAddressOn:(nullable IRCClient *)client { return (self.length > 0 && [self containsCharacters:@"\x021\x040\x000\x020\x00d\x00a"] == NO); } - (BOOL)isHostmaskUsername { return [self isHostmaskUsernameOn:nil]; } - (BOOL)isHostmaskUsernameOn:(nullable IRCClient *)client { return (self.length > 0 && self.length <= TXMaximumIRCUsernameLength && [self containsCharacters:@"\x000\x020\x00d\x00a"] == NO); } - (BOOL)isHostmaskNickname { return [self isHostmaskNicknameOn:nil]; } - (BOOL)isHostmaskNicknameOn:(nullable IRCClient *)client { NSUInteger maximumLength = TXMaximumIRCNicknameLength; if (client) { /* At least one server has been found (gitter.im) has been found which does not send a configuration profile. They allow a nickname length larger than IRCISupportInfo uses as a default which means parsing will go wonky. */ /* A smarter workaround would probably to check if specific configuration options were received (e.g. "NICKLEN"), but that has more overhead than using a boolean. */ if (client.supportInfo.configurationReceived) { maximumLength = client.supportInfo.maximumNicknameLength; } else { maximumLength = 0; } /* If we are connected to ZNC, then do not enforce maximum nickname length. It is easier to disable this check than to check whether a nickname (e.g. *buffextras) should be handled differently. */ if (client.isConnectedToZNC) { maximumLength = 0; } } if (maximumLength == 0) { maximumLength = TXMaximumIRCNicknameLength; } return ([self isNotEqualTo:@"*"] && self.length > 0 && self.length <= maximumLength && [self containsCharacters:@"\x021\x040\x000\x020\x00d\x00a"] == NO); } - (BOOL)isChannelNameOn:(IRCClient *)client { NSParameterAssert(client != nil); if (self.length == 0) { return NO; } /* A maximum length is not current imposed on channel names, even though we have that information, because -isChannelNameOn: is called in -[IRCClient sendCommand:...] to check whether the the input string for a command begins with a channel name before we perform tokenizing. During that time, the input string can be any possible length. We therefore only check if the string begins with known channel name prefixes and allow server to perform extensive validation on the channel name. */ NSArray *channelNamePrefixes = client.supportInfo.channelNamePrefixes; if (self.length == 1) { NSString *character = [self stringCharacterAtIndex:0]; return [channelNamePrefixes containsObject:character]; } NSString *character1 = [self stringCharacterAtIndex:0]; NSString *character2 = [self stringCharacterAtIndex:1]; /* The ~ prefix is considered special. It is used by the ZNC partyline plugin. */ BOOL isPartyline = ([character1 isEqualToString:@"~"] && [character2 isEqualToString:@"#"]); return (isPartyline || [channelNamePrefixes containsObject:character1]); } - (BOOL)isChannelName { if (self.length == 0) { return NO; } UniChar c = [self characterAtIndex:0]; return (c == '#' || c == '&' || c == '+' || c == '!' || c == '~' || c == '?'); } - (nullable NSString *)channelNameWithoutBang { if (self.isChannelName == NO) { return self; } return [self substringFromIndex:1]; } - (nullable NSString *)channelNameWithoutBangOn:(IRCClient *)client { NSParameterAssert(client != nil); if ([self isChannelNameOn:client] == NO) { return self; } if (self.length < 2) { // Do not turn "#" into empty string return self; } NSArray *channelNamePrefixes = client.supportInfo.channelNamePrefixes; NSString *character = [self stringCharacterAtIndex:0]; if ([channelNamePrefixes containsObject:character]) { return [self substringFromIndex:1]; } return self; } - (nullable NSString *)nicknameFromHostmask { NSString *nickname = nil; if ([self hostmaskComponents:&nickname username:nil address:nil]) { return nickname; } return self; } - (nullable NSString *)usernameFromHostmask { NSString *username = nil; if ([self hostmaskComponents:nil username:&username address:nil]) { return username; } return nil; } - (nullable NSString *)addressFromHostmask { NSString *address = nil; if ([self hostmaskComponents:nil username:nil address:&address]) { return address; } return nil; } - (nullable NSString *)stringWithValidURIScheme { return [AHHyperlinkScanner URLWithProperScheme:self]; } - (nullable NSAttributedString *)attributedStringWithIRCFormatting:(NSFont *)preferredFont preferredFontColor:(nullable NSColor *)preferredFontColor honorFormattingPreference:(BOOL)formattingPreference { if (formattingPreference && [TPCPreferences removeAllFormatting]) { NSString *string = self.stripIRCEffects; return [NSAttributedString attributedStringWithString:string]; } NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; if (preferredFont) { attributes[TVCLogRendererConfigurationAttributedStringPreferredFontAttribute] = preferredFont; } if (preferredFontColor) { attributes[TVCLogRendererConfigurationAttributedStringPreferredFontColorAttribute] = preferredFontColor; } return [TVCLogRenderer renderBodyAsAttributedString:self withAttributes:attributes]; } - (nullable NSAttributedString *)attributedStringWithIRCFormatting:(NSFont *)preferredFont preferredFontColor:(nullable NSColor *)preferredFontColor { return [self attributedStringWithIRCFormatting:preferredFont preferredFontColor:preferredFontColor honorFormattingPreference:NO]; } - (NSString *)stripIRCEffects { NSUInteger stringLength = self.length; if (stringLength == 0) { return self; } NSUInteger currentPosition = 0; NSUInteger bufferLength = (stringLength * sizeof(UniChar)); UniChar *inputBuffer = alloca(bufferLength); UniChar *outputBuffer = alloca(bufferLength); [self getCharacters:inputBuffer range:self.range]; for (NSUInteger i = 0; i < stringLength; i++) { UniChar character = inputBuffer[i]; switch (character) { case IRCTextFormatterEffectBoldCharacter: case IRCTextFormatterEffectItalicCharacter: case IRCTextFormatterEffectItalicCharacterOld: case IRCTextFormatterEffectMonospaceCharacter: case IRCTextFormatterEffectStrikethroughCharacter: case IRCTextFormatterEffectUnderlineCharacter: case IRCTextFormatterTerminatingCharacter: { break; } case IRCTextFormatterEffectColorAsDigitCharacter: case IRCTextFormatterEffectColorAsHexCharacter: { // One is subtracted because the for loop will increment by one for us i += ([self colorComponentsOfCharacter:character startingAt:i foregroundColor:NULL backgroundColor:NULL] - 1); break; } default: { outputBuffer[currentPosition++] = character; break; } } } return [NSString stringWithCharacters:outputBuffer length:currentPosition]; } - (NSUInteger)colorComponentsOfCharacter:(UniChar)character startingAt:(NSUInteger)rangeStart foregroundColor:(id _Nullable * _Nullable)foregroundColor backgroundColor:(id _Nullable * _Nullable)backgroundColor { if (character == IRCTextFormatterEffectColorAsDigitCharacter) { return [self colorAsDigitStartingAt:rangeStart foregroundColor:foregroundColor backgroundColor:backgroundColor]; } else if (character == IRCTextFormatterEffectColorAsHexCharacter) { return [self colorAsHexStartingAt:rangeStart foregroundColor:foregroundColor backgroundColor:backgroundColor]; } return 0; } - (NSUInteger)colorAsHexStartingAt:(NSUInteger)rangeStart foregroundColor:(NSColor * _Nullable * _Nullable)foregroundColor backgroundColor:(NSColor * _Nullable * _Nullable)backgroundColor { NSUInteger selfLength = self.length; NSParameterAssert(rangeStart < selfLength); NSUInteger currentPosition = rangeStart; NSString *m_foregroundColor = nil; NSString *m_backgroundColor = nil; BOOL commaEaten = NO; // ========================================== // /* Control character */ currentPosition++; // ========================================== // /* Foreground hex color */ if ((currentPosition + 6) > selfLength) { goto return_method; } m_foregroundColor = [self substringWithRange:NSMakeRange(currentPosition, 6)]; if ([m_foregroundColor onlyContainsCharactersFromCharacterSet:[NSCharacterSet hexadecimalCharacterSet]]) { currentPosition += 6; // Eat foreground color } else { m_foregroundColor = nil; goto return_method; } // ========================================== // /* Comma */ if (currentPosition >= selfLength) { goto return_method; } UniChar a = [self characterAtIndex:currentPosition]; if (a == ',') { commaEaten = YES; currentPosition++; // Eat comma } else { goto return_method; } // ========================================== // /* Background hex color */ if ((currentPosition + 6) > selfLength) { goto return_method; } m_backgroundColor = [self substringWithRange:NSMakeRange(currentPosition, 6)]; if ([m_backgroundColor onlyContainsCharactersFromCharacterSet:[NSCharacterSet hexadecimalCharacterSet]]) { currentPosition += 6; // Eat background color } else { m_backgroundColor = nil; } // ========================================== // return_method: if (m_backgroundColor == nil && commaEaten) { currentPosition -= 1; } if ( foregroundColor && m_foregroundColor != nil) { *foregroundColor = [NSColor colorWithHexadecimalValue:m_foregroundColor.uppercaseString]; } if ( backgroundColor && m_backgroundColor != nil) { *backgroundColor = [NSColor colorWithHexadecimalValue:m_backgroundColor.uppercaseString]; } return (currentPosition - rangeStart); } - (NSUInteger)colorAsDigitStartingAt:(NSUInteger)rangeStart foregroundColor:(NSNumber * _Nullable * _Nullable)foregroundColor backgroundColor:(NSNumber * _Nullable * _Nullable)backgroundColor { NSUInteger selfLength = self.length; NSParameterAssert(rangeStart < selfLength); NSUInteger currentPosition = rangeStart; NSUInteger m_foregroundColor = NSNotFound; NSUInteger m_backgroundColor = NSNotFound; BOOL commaEaten = NO; // ========================================== // /* Control character */ currentPosition++; // ========================================== // /* Foreground color first color number */ if (currentPosition >= selfLength) { goto return_method; } UniChar a = [self characterAtIndex:currentPosition]; if (CS_StringIsBase10Numeric(a) == NO) { goto return_method; } m_foregroundColor = (a - '0'); currentPosition++; // Eat first color number // ========================================== // /* Foreground color second color number */ if (currentPosition >= selfLength) { goto return_method; } UniChar b = [self characterAtIndex:currentPosition]; if (CS_StringIsBase10Numeric(b)) { m_foregroundColor = (m_foregroundColor * 10 + b - '0'); currentPosition++; // Eat second color number } // ========================================== // /* Comma */ if (currentPosition >= selfLength) { goto return_method; } UniChar c = [self characterAtIndex:currentPosition]; if (c == ',') { commaEaten = YES; currentPosition++; // Eat comma } else { goto return_method; } // ========================================== // /* Background color first color number */ if (currentPosition >= selfLength) { goto return_method; } UniChar d = [self characterAtIndex:currentPosition]; if (CS_StringIsBase10Numeric(d) == NO) { goto return_method; } m_backgroundColor = (d - '0'); currentPosition++; // Eat first color number // ========================================== // /* Background color second color number */ if (currentPosition >= selfLength) { goto return_method; } UniChar e = [self characterAtIndex:currentPosition]; if (CS_StringIsBase10Numeric(e) == NO) { goto return_method; } m_backgroundColor = (m_backgroundColor * 10 + e - '0'); currentPosition++; // Eat second color number // ========================================== // return_method: if (m_backgroundColor == NSNotFound && commaEaten) { currentPosition -= 1; } if ( foregroundColor && m_foregroundColor != NSNotFound && m_foregroundColor <= IRCTextFormatterEffectColorHighestDigit) { *foregroundColor = @(m_foregroundColor); } if ( backgroundColor && m_backgroundColor != NSNotFound && m_backgroundColor <= IRCTextFormatterEffectColorHighestDigit) { *backgroundColor = @(m_backgroundColor); } return (currentPosition - rangeStart); } - (NSArray *)base64EncodingWithLineLength:(NSUInteger)lineLength { if (self.length == 0) { return @[self]; } NSData *selfData = [self dataUsingEncoding:NSUTF8StringEncoding]; NSString *encodedString = [XRBase64Encoding encodeData:selfData]; return [encodedString splitWithMaximumLength:lineLength]; } - (nullable NSString *)padNicknameWithCharacter:(UniChar)padCharacter maximumLength:(NSUInteger)maximumLength { NSParameterAssert(padCharacter != 0); NSParameterAssert(maximumLength > 0); NSString *padCharacterString = [NSString stringWithUniChar:padCharacter]; if (self.length < maximumLength) { return [self stringByAppendingString:padCharacterString]; } NSString *substring = [self substringToIndex:maximumLength]; for (NSInteger i = (substring.length - 1); i >= 0; i--) { UniChar substringCharacter = [substring characterAtIndex:i]; if (substringCharacter == padCharacter) { continue; } NSString *stringHead = [substring substringToIndex:i]; NSMutableString *stringHeadMutable = [stringHead mutableCopy]; for (NSUInteger j = i; j < substring.length; j++) { [stringHeadMutable appendString:@"_"]; } return [stringHeadMutable copy]; } return nil; } - (nullable NSString *)prettyLicenseKey { #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 if (self.length == 0) { return nil; } NSRange lastDashRange = [self rangeOfString:@"-" options:NSBackwardsSearch]; if (lastDashRange.location == NSNotFound) { return nil; } /* Go from dash outward by 5 */ lastDashRange.length = (lastDashRange.location + 6); // 1 = dash, 5 = numbers (1+5) lastDashRange.location = 0; NSString *licenseKey = [self substringWithRange:lastDashRange]; return [licenseKey stringByAppendingString:@"…"]; #else return nil; #endif } /* Source: https://ircv3.net/specs/core/message-tags-3.2.html */ - (NSString *)encodedMessageTagString { if (self.length == 0) { return self; } NSMutableString *bob = [self mutableCopy]; [bob replaceOccurrencesOfString:@"\\" withString:@"\\\\" options:0 range:bob.range]; [bob replaceOccurrencesOfString:@";" withString:@"\\:" options:0 range:bob.range]; [bob replaceOccurrencesOfString:@" " withString:@"\\s" options:0 range:bob.range]; [bob replaceOccurrencesOfString:@"\r" withString:@"\\r" options:0 range:bob.range]; [bob replaceOccurrencesOfString:@"\n" withString:@"\\n" options:0 range:bob.range]; return [bob copy]; } - (NSString *)decodedMessageTagString { if (self.length == 0) { return self; } NSMutableString *bob = [self mutableCopy]; if ([bob hasSuffix:@"\\"]) { [bob deleteCharactersInRange:NSMakeRange((bob.length - 1), 1)]; } [bob replaceOccurrencesOfString:@"\\:" withString:@";" options:0 range:bob.range]; [bob replaceOccurrencesOfString:@"\\\\" withString:@"\\" options:0 range:bob.range]; [bob replaceOccurrencesOfString:@"\\s" withString:@" " options:0 range:bob.range]; [bob replaceOccurrencesOfString:@"\\r" withString:@"\r" options:0 range:bob.range]; [bob replaceOccurrencesOfString:@"\\n" withString:@"\n" options:0 range:bob.range]; return [bob copy]; } - (BOOL)isModeSymbol { if (self.length != 1) { return NO; } return [[NSCharacterSet AtoZCharacterSet] characterIsMember:[self characterAtIndex:0]]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Helpers/Cocoa (Objective-C)/NSTableViewHelper.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @implementation NSTableView (TXTableViewHelper) + (NSFont *)preferredGlobalTableViewFont { return [NSFont systemFontOfSize:13.0]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Helpers/Cocoa (Objective-C)/NSViewHelper.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMainWindow.h" NS_ASSUME_NONNULL_BEGIN @implementation NSWindow (TXWindowHelper) - (void)changeFrameToMin { [self changeFrameToMinAndDisplay:YES animate:NO]; } - (void)changeFrameToMinAndDisplay:(BOOL)display { [self changeFrameToMinAndDisplay:display animate:NO]; } - (void)changeFrameToMinAndDisplay:(BOOL)display animate:(BOOL)animate { NSSize minSize = self.contentMinSize; [self changeFrameTo:minSize display:display animate:NO]; } - (void)changeFrameTo:(NSSize)minSize display:(BOOL)display animate:(BOOL)animate { NSRect oldFrame = self.frame; NSRect newFrame = oldFrame; NSRect contentRect = [self contentRectForFrameRect:newFrame]; float bezelHeight = (newFrame.size.height - contentRect.size.height); newFrame.size.width = minSize.width; newFrame.size.height = (bezelHeight + minSize.height); newFrame.origin.y = (NSMaxY(oldFrame) - newFrame.size.height); [self setFrame:newFrame display:display animate:animate]; } - (void)replaceContentView:(NSView *)withView { self.contentView = nil; [self changeFrameTo:withView.frame.size display:NO animate:NO]; self.contentView = withView; } @end #pragma mark - @implementation NSView (TXViewHelperPrivate) - (nullable TVCMainWindow *)mainWindow { NSWindow *window = self.window; if ([window isMemberOfClass:[TVCMainWindow class]] == NO) { return nil; } return (TVCMainWindow *)window; } @end #pragma mark - @implementation NSView (TXViewHelper) - (void)addConstraintsToSuperviewToHugEdges { NSView *superview = self.superview; NSArray *constraints = @[ [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0] ]; [superview addConstraints:constraints]; } - (void)addConstraintsToSuperviewToEqualDimensions { NSView *superview = self.superview; NSMutableArray *constraints = [NSMutableArray array]; [constraints addObjectsFromArray: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[self(0@550)]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(self)]]; [constraints addObjectsFromArray: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[self(0@550)]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(self)]]; [superview addConstraints:constraints]; } - (void)replaceFirstSubview:(NSView *)withSubview { NSParameterAssert(withSubview != nil); /* Remove any views that may already be in place. */ [self.subviews.firstObject removeFromSuperviewWithoutNeedingDisplay]; /* Add subview. */ [self addSubview:withSubview]; /* Apply constraints. */ [withSubview addConstraintsToSuperviewToHugEdges]; [withSubview addConstraintsToSuperviewToEqualDimensions]; } @end #pragma mark - @implementation NSCell (TXCellHelperPrivate) - (nullable NSWindow *)window { return self.controlView.window; } - (nullable TVCMainWindow *)mainWindow { return self.controlView.mainWindow; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Helpers/Cocoa (Objective-C)/TXAppearanceHelper.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual and/or Codeux Software, nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXAppearanceHelper.h" NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark NSView Category @implementation NSView (TXAppearance) - (BOOL)needsDisplayWhenApplicationAppearanceChanges { return NO; } - (BOOL)needsDisplayWhenSystemAppearanceChanges { return NO; } - (BOOL)sendApplicationAppearanceChangedToSubviews { return YES; } - (BOOL)sendSystemAppearanceChangedToSubviews { return YES; } - (void)applicationAppearanceChanged { if (self.needsDisplayWhenApplicationAppearanceChanges) { self.needsDisplay = YES; } } - (void)systemAppearanceChanged { if (self.needsDisplayWhenSystemAppearanceChanges) { self.needsDisplay = YES; } } - (void)notifyApplicationAppearanceChanged { [self applicationAppearanceChanged]; if (self.sendApplicationAppearanceChangedToSubviews == NO) { return; } for (NSView *view in self.subviews) { [view notifyApplicationAppearanceChanged]; } } - (void)notifySystemAppearanceChanged { [self systemAppearanceChanged]; if (self.sendSystemAppearanceChangedToSubviews == NO) { return; } for (NSView *view in self.subviews) { [view notifySystemAppearanceChanged]; } } @end #pragma mark - #pragma mark NSWindow Category @implementation NSWindow (TXApplication) - (void)notifyApplicationAppearanceChanged { [self.contentView.superview notifyApplicationAppearanceChanged]; for (NSTitlebarAccessoryViewController *viewController in self.titlebarAccessoryViewControllers) { [viewController.view notifyApplicationAppearanceChanged]; } } - (void)notifySystemAppearanceChanged { [self.contentView.superview notifySystemAppearanceChanged]; for (NSTitlebarAccessoryViewController *viewController in self.titlebarAccessoryViewControllers) { [viewController.view notifySystemAppearanceChanged]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Helpers/External Libraries/Google/GTMEncodeHTML.m ================================================ // // GTMNSString+HTML.m // Dealing with NSStrings that contain HTML // // Copyright 2006-2008 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. // #import "GTMEncodeHTML.h" typedef struct { NSString *escapeSequence; unichar uchar; } HTMLEscapeMap; // Taken from http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters // Ordered by uchar lowest to highest for bsearching static HTMLEscapeMap gAsciiHTMLEscapeMap[] = { // A.2.2. Special characters { @""", 34 }, { @"&", 38 }, { @"'", 39 }, { @"<", 60 }, { @">", 62 }, // A.2.1. Latin-1 characters { @" ", 160 }, { @"¡", 161 }, { @"¢", 162 }, { @"£", 163 }, { @"¤", 164 }, { @"¥", 165 }, { @"¦", 166 }, { @"§", 167 }, { @"¨", 168 }, { @"©", 169 }, { @"ª", 170 }, { @"«", 171 }, { @"¬", 172 }, { @"­", 173 }, { @"®", 174 }, { @"¯", 175 }, { @"°", 176 }, { @"±", 177 }, { @"²", 178 }, { @"³", 179 }, { @"´", 180 }, { @"µ", 181 }, { @"¶", 182 }, { @"·", 183 }, { @"¸", 184 }, { @"¹", 185 }, { @"º", 186 }, { @"»", 187 }, { @"¼", 188 }, { @"½", 189 }, { @"¾", 190 }, { @"¿", 191 }, { @"À", 192 }, { @"Á", 193 }, { @"Â", 194 }, { @"Ã", 195 }, { @"Ä", 196 }, { @"Å", 197 }, { @"Æ", 198 }, { @"Ç", 199 }, { @"È", 200 }, { @"É", 201 }, { @"Ê", 202 }, { @"Ë", 203 }, { @"Ì", 204 }, { @"Í", 205 }, { @"Î", 206 }, { @"Ï", 207 }, { @"Ð", 208 }, { @"Ñ", 209 }, { @"Ò", 210 }, { @"Ó", 211 }, { @"Ô", 212 }, { @"Õ", 213 }, { @"Ö", 214 }, { @"×", 215 }, { @"Ø", 216 }, { @"Ù", 217 }, { @"Ú", 218 }, { @"Û", 219 }, { @"Ü", 220 }, { @"Ý", 221 }, { @"Þ", 222 }, { @"ß", 223 }, { @"à", 224 }, { @"á", 225 }, { @"â", 226 }, { @"ã", 227 }, { @"ä", 228 }, { @"å", 229 }, { @"æ", 230 }, { @"ç", 231 }, { @"è", 232 }, { @"é", 233 }, { @"ê", 234 }, { @"ë", 235 }, { @"ì", 236 }, { @"í", 237 }, { @"î", 238 }, { @"ï", 239 }, { @"ð", 240 }, { @"ñ", 241 }, { @"ò", 242 }, { @"ó", 243 }, { @"ô", 244 }, { @"õ", 245 }, { @"ö", 246 }, { @"÷", 247 }, { @"ø", 248 }, { @"ù", 249 }, { @"ú", 250 }, { @"û", 251 }, { @"ü", 252 }, { @"ý", 253 }, { @"þ", 254 }, { @"ÿ", 255 }, // A.2.2. Special characters cont'd { @"Œ", 338 }, { @"œ", 339 }, { @"Š", 352 }, { @"š", 353 }, { @"Ÿ", 376 }, // A.2.3. Symbols { @"ƒ", 402 }, // A.2.2. Special characters cont'd { @"ˆ", 710 }, { @"˜", 732 }, // A.2.3. Symbols cont'd { @"Α", 913 }, { @"Β", 914 }, { @"Γ", 915 }, { @"Δ", 916 }, { @"Ε", 917 }, { @"Ζ", 918 }, { @"Η", 919 }, { @"Θ", 920 }, { @"Ι", 921 }, { @"Κ", 922 }, { @"Λ", 923 }, { @"Μ", 924 }, { @"Ν", 925 }, { @"Ξ", 926 }, { @"Ο", 927 }, { @"Π", 928 }, { @"Ρ", 929 }, { @"Σ", 931 }, { @"Τ", 932 }, { @"Υ", 933 }, { @"Φ", 934 }, { @"Χ", 935 }, { @"Ψ", 936 }, { @"Ω", 937 }, { @"α", 945 }, { @"β", 946 }, { @"γ", 947 }, { @"δ", 948 }, { @"ε", 949 }, { @"ζ", 950 }, { @"η", 951 }, { @"θ", 952 }, { @"ι", 953 }, { @"κ", 954 }, { @"λ", 955 }, { @"μ", 956 }, { @"ν", 957 }, { @"ξ", 958 }, { @"ο", 959 }, { @"π", 960 }, { @"ρ", 961 }, { @"ς", 962 }, { @"σ", 963 }, { @"τ", 964 }, { @"υ", 965 }, { @"φ", 966 }, { @"χ", 967 }, { @"ψ", 968 }, { @"ω", 969 }, { @"ϑ", 977 }, { @"ϒ", 978 }, { @"ϖ", 982 }, // A.2.2. Special characters cont'd { @" ", 8194 }, { @" ", 8195 }, { @" ", 8201 }, { @"‌", 8204 }, { @"‍", 8205 }, { @"‎", 8206 }, { @"‏", 8207 }, { @"–", 8211 }, { @"—", 8212 }, { @"‘", 8216 }, { @"’", 8217 }, { @"‚", 8218 }, { @"“", 8220 }, { @"”", 8221 }, { @"„", 8222 }, { @"†", 8224 }, { @"‡", 8225 }, // A.2.3. Symbols cont'd { @"•", 8226 }, { @"…", 8230 }, // A.2.2. Special characters cont'd { @"‰", 8240 }, // A.2.3. Symbols cont'd { @"′", 8242 }, { @"″", 8243 }, // A.2.2. Special characters cont'd { @"‹", 8249 }, { @"›", 8250 }, // A.2.3. Symbols cont'd { @"‾", 8254 }, { @"⁄", 8260 }, // A.2.2. Special characters cont'd { @"€", 8364 }, // A.2.3. Symbols cont'd { @"ℑ", 8465 }, { @"℘", 8472 }, { @"ℜ", 8476 }, { @"™", 8482 }, { @"ℵ", 8501 }, { @"←", 8592 }, { @"↑", 8593 }, { @"→", 8594 }, { @"↓", 8595 }, { @"↔", 8596 }, { @"↵", 8629 }, { @"⇐", 8656 }, { @"⇑", 8657 }, { @"⇒", 8658 }, { @"⇓", 8659 }, { @"⇔", 8660 }, { @"∀", 8704 }, { @"∂", 8706 }, { @"∃", 8707 }, { @"∅", 8709 }, { @"∇", 8711 }, { @"∈", 8712 }, { @"∉", 8713 }, { @"∋", 8715 }, { @"∏", 8719 }, { @"∑", 8721 }, { @"−", 8722 }, { @"∗", 8727 }, { @"√", 8730 }, { @"∝", 8733 }, { @"∞", 8734 }, { @"∠", 8736 }, { @"∧", 8743 }, { @"∨", 8744 }, { @"∩", 8745 }, { @"∪", 8746 }, { @"∫", 8747 }, { @"∴", 8756 }, { @"∼", 8764 }, { @"≅", 8773 }, { @"≈", 8776 }, { @"≠", 8800 }, { @"≡", 8801 }, { @"≤", 8804 }, { @"≥", 8805 }, { @"⊂", 8834 }, { @"⊃", 8835 }, { @"⊄", 8836 }, { @"⊆", 8838 }, { @"⊇", 8839 }, { @"⊕", 8853 }, { @"⊗", 8855 }, { @"⊥", 8869 }, { @"⋅", 8901 }, { @"⌈", 8968 }, { @"⌉", 8969 }, { @"⌊", 8970 }, { @"⌋", 8971 }, { @"⟨", 9001 }, { @"⟩", 9002 }, { @"◊", 9674 }, { @"♠", 9824 }, { @"♣", 9827 }, { @"♥", 9829 }, { @"♦", 9830 } }; // Taken from http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters // This is table A.2.2 Special Characters static HTMLEscapeMap gUnicodeHTMLEscapeMap[] = { // C0 Controls and Basic Latin { @""", 34 }, { @"&", 38 }, { @"'", 39 }, { @"<", 60 }, { @">", 62 }, // Latin Extended-A { @"Œ", 338 }, { @"œ", 339 }, { @"Š", 352 }, { @"š", 353 }, { @"Ÿ", 376 }, // Spacing Modifier Letters { @"ˆ", 710 }, { @"˜", 732 }, // General Punctuation { @" ", 8194 }, { @" ", 8195 }, { @" ", 8201 }, { @"‌", 8204 }, { @"‍", 8205 }, { @"‎", 8206 }, { @"‏", 8207 }, { @"–", 8211 }, { @"—", 8212 }, { @"‘", 8216 }, { @"’", 8217 }, { @"‚", 8218 }, { @"“", 8220 }, { @"”", 8221 }, { @"„", 8222 }, { @"†", 8224 }, { @"‡", 8225 }, { @"‰", 8240 }, { @"‹", 8249 }, { @"›", 8250 }, { @"€", 8364 }, }; // Utility function for Bsearching table above static int EscapeMapCompare(const void *ucharVoid, const void *mapVoid) { const unichar *uchar = (const unichar*)ucharVoid; const HTMLEscapeMap *map = (const HTMLEscapeMap*)mapVoid; int val; if (*uchar > map->uchar) { val = 1; } else if (*uchar < map->uchar) { val = -1; } else { val = 0; } return val; } static NSString *StringByEscapingHTMLUsingTable(NSString *src, HTMLEscapeMap* table, NSUInteger tableSize, BOOL escapeUnicode) { NSUInteger length = [src length]; if (!length) { return src; } NSMutableString *finalString = [NSMutableString string]; NSMutableData *data2 = [NSMutableData dataWithCapacity:sizeof(unichar) * length]; // this block is common between GTMNSString+HTML and GTMNSString+XML but // it's so short that it isn't really worth trying to share. const unichar *buffer = CFStringGetCharactersPtr((CFStringRef)src); if (!buffer) { // We want this buffer to be autoreleased. NSMutableData *data = [NSMutableData dataWithLength:length * sizeof(UniChar)]; if (!data) { // COV_NF_START - Memory fail case // If we can't get enough memory for the buffer copy, odds are finalString // will also run out of memory, so just give up. NSCAssert(NO, @"couldn't alloc buffer"); // COV_NF_END } [src getCharacters:[data mutableBytes]]; buffer = [data bytes]; } if (!buffer || !data2) { // If we can't get enough memory for the buffer copy, odds are finalString // will also run out of memory, so just give up. NSCAssert(NO, @"Unable to allocate buffer or data2"); } unichar *buffer2 = (unichar *)[data2 mutableBytes]; NSUInteger buffer2Length = 0; for (NSUInteger i = 0; i < length; ++i) { HTMLEscapeMap *val = bsearch(&buffer[i], table, tableSize / sizeof(HTMLEscapeMap), sizeof(HTMLEscapeMap), EscapeMapCompare); if (val || (escapeUnicode && buffer[i] > 127)) { if (buffer2Length) { CFStringAppendCharacters((CFMutableStringRef)finalString, buffer2, buffer2Length); buffer2Length = 0; } if (val) { [finalString appendString:val->escapeSequence]; } else { NSCAssert((escapeUnicode && buffer[i] > 127), @"Illegal Character"); [finalString appendFormat:@"&#%d;", buffer[i]]; } } else { buffer2[buffer2Length] = buffer[i]; buffer2Length += 1; } } if (buffer2Length) { CFStringAppendCharacters((CFMutableStringRef)finalString, buffer2, buffer2Length); } return finalString; } @implementation NSString (GTMNSStringHTMLAdditions) - (NSString *)gtm_stringByEscapingForHTML { return StringByEscapingHTMLUsingTable(self, gUnicodeHTMLEscapeMap, sizeof(gUnicodeHTMLEscapeMap), /*escapingUnicode=*/NO); } // gtm_stringByEscapingHTML - (NSString *)gtm_stringByEscapingForAsciiHTML { return StringByEscapingHTMLUsingTable(self, gAsciiHTMLEscapeMap, sizeof(gAsciiHTMLEscapeMap), /*escapingUnicode=*/YES); } // gtm_stringByEscapingAsciiHTML - (NSString *)gtm_stringByUnescapingFromHTML { NSRange range = NSMakeRange(0, [self length]); NSRange subrange = [self rangeOfString:@"&" options:NSBackwardsSearch range:range]; // if no ampersands, we've got a quick way out if (subrange.length == 0) return self; NSMutableString *finalString = [NSMutableString stringWithString:self]; do { NSRange semiColonRange = NSMakeRange(subrange.location, NSMaxRange(range) - subrange.location); semiColonRange = [self rangeOfString:@";" options:0 range:semiColonRange]; range = NSMakeRange(0, subrange.location); // if we don't find a semicolon in the range, we don't have a sequence if (semiColonRange.location == NSNotFound) { continue; } NSRange escapeRange = NSMakeRange(subrange.location, semiColonRange.location - subrange.location + 1); NSString *escapeString = [self substringWithRange:escapeRange]; NSUInteger length = [escapeString length]; // a squence must be longer than 3 (<) and less than 11 (ϑ) if (length > 3 && length < 11) { if ([escapeString characterAtIndex:1] == '#') { unichar char2 = [escapeString characterAtIndex:2]; if (char2 == 'x' || char2 == 'X') { // Hex escape squences £ NSString *hexSequence = [escapeString substringWithRange:NSMakeRange(3, length - 4)]; NSScanner *scanner = [NSScanner scannerWithString:hexSequence]; unsigned value; if ([scanner scanHexInt:&value] && value > 0 && [scanner scanLocation] == length - 4) { if (value < USHRT_MAX) { unichar uchar = (unichar)value; NSString *charString = [NSString stringWithCharacters:&uchar length:1]; [finalString replaceCharactersInRange:escapeRange withString:charString]; } else if (value >= 0x10000 && value <= 0x10FFFF) { // code points in unicode supplementary planes int subtractedValue = value - 0x10000; unichar uchars[2]; uchars[0] = 0xD800 + (subtractedValue >> 10); uchars[1] = 0xDC00 + (subtractedValue & 0x3FF); NSString *charString = [NSString stringWithCharacters:uchars length:2]; if (charString) { [finalString replaceCharactersInRange:escapeRange withString:charString]; } } } } else { // Decimal Sequences { NSString *numberSequence = [escapeString substringWithRange:NSMakeRange(2, length - 3)]; NSScanner *scanner = [NSScanner scannerWithString:numberSequence]; int value; if ([scanner scanInt:&value] && value > 0 && [scanner scanLocation] == length - 3) { if (value < USHRT_MAX) { unichar uchar = (unichar)value; NSString *charString = [NSString stringWithCharacters:&uchar length:1]; [finalString replaceCharactersInRange:escapeRange withString:charString]; } else if (value >= 0x10000 && value <= 0x10FFFF) { // code points in unicode supplementary planes int subtractedValue = value - 0x10000; unichar uchars[2]; uchars[0] = 0xD800 + (subtractedValue >> 10); uchars[1] = 0xDC00 + (subtractedValue & 0x3FF); NSString *charString = [NSString stringWithCharacters:uchars length:2]; if (charString) { [finalString replaceCharactersInRange:escapeRange withString:charString]; } } } } } else { // "standard" sequences for (unsigned i = 0; i < sizeof(gAsciiHTMLEscapeMap) / sizeof(HTMLEscapeMap); ++i) { if ([escapeString isEqualToString:gAsciiHTMLEscapeMap[i].escapeSequence]) { [finalString replaceCharactersInRange:escapeRange withString:[NSString stringWithCharacters:&gAsciiHTMLEscapeMap[i].uchar length:1]]; break; } } } } } while ((subrange = [self rangeOfString:@"&" options:NSBackwardsSearch range:range]).length != 0); return finalString; } // gtm_stringByUnescapingHTML @end ================================================ FILE: Sources/App/Classes/Helpers/External Libraries/WebKit/WebScriptObjectHelper.m ================================================ /* ********************************************************************* Copyright (c) 2010 - 2018 Codeux Software, LLC Please see ACKNOWLEDGEMENT for additional information. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of "Codeux Software, LLC", nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *********************************************************************** */ TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN #import "WebScriptObjectHelperPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation WebScriptObject (TXWebScriptObjectHelper) - (nullable id)toCommonInContext:(JSContextRef)jsContextRef { NSParameterAssert(jsContextRef != NULL); JSObjectRef jsObjectRef = [self JSObject]; /* The object is useless if it is a function */ if (JSObjectIsFunction(jsContextRef, jsObjectRef)) { LogToConsoleDebug("Ignoring a JSObject that is a function"); return nil; } /* If the object is an array, then parse it as such */ if ([WebScriptObject jsObjectIsArray:jsObjectRef inContext:jsContextRef]) { NSNumber *arrayLengthObject = [self valueForKey:@"length"]; NSUInteger arrayLength = arrayLengthObject.unsignedIntegerValue; NSMutableArray *scriptArray = [NSMutableArray arrayWithCapacity:arrayLength]; for (NSUInteger i = 0; i < arrayLength; i++) { id item = [self webScriptValueAtIndex:(unsigned)i]; if ([item isKindOfClass:[WebScriptObject class]]) { item = [item toCommonInContext:jsContextRef]; } else if ([item isKindOfClass:[WebUndefined class]]) { item = nil; } if (item) { [scriptArray addObject:item]; } else { [scriptArray addObject:[NSNull null]]; } } return scriptArray; } /* If the object is an object (dictionary), then parse it as such */ if ([WebScriptObject jsObjectIsObject:jsObjectRef inContext:jsContextRef]) { JSPropertyNameArrayRef objectProperties = JSObjectCopyPropertyNames(jsContextRef, jsObjectRef); size_t objectPropertiesCount = JSPropertyNameArrayGetCount(objectProperties); NSMutableDictionary *scriptDictionary = [NSMutableDictionary dictionaryWithCapacity:(NSUInteger)objectPropertiesCount]; for (NSUInteger i = 0; i < objectPropertiesCount; i++) { JSStringRef propertyName = JSPropertyNameArrayGetNameAtIndex(objectProperties, i); NSString *propertyNameCocoa = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, propertyName); id item = [self valueForKey:propertyNameCocoa]; if ([item isKindOfClass:[WebScriptObject class]]) { item = [item toCommonInContext:jsContextRef]; } else if ([item isKindOfClass:[WebUndefined class]]) { item = nil; } if (item) { scriptDictionary[propertyNameCocoa] = item; } else { scriptDictionary[propertyNameCocoa] = [NSNull null]; } } return scriptDictionary; } return nil; } + (BOOL)jsObjectIsArray:(JSObjectRef)jsObjectRef inContext:(JSContextRef)jsContextRef { NSParameterAssert(jsObjectRef != NULL); NSParameterAssert(jsContextRef != NULL); JSObjectRef jsGlobalObjectRef = JSContextGetGlobalObject(jsContextRef); JSStringRef arrayString = JSStringCreateWithUTF8CString("Array"); JSObjectRef arrayPrototype = (JSObjectRef)JSObjectGetProperty(jsContextRef, jsGlobalObjectRef, arrayString, NULL); JSStringRelease(arrayString); return JSValueIsInstanceOfConstructor(jsContextRef, jsObjectRef, arrayPrototype, NULL); } + (BOOL)jsObjectIsObject:(JSObjectRef)jsObjectRef inContext:(JSContextRef)jsContextRef { NSParameterAssert(jsObjectRef != NULL); NSParameterAssert(jsContextRef != NULL); JSObjectRef jsGlobalObjectRef = JSContextGetGlobalObject(jsContextRef); JSStringRef objectString = JSStringCreateWithUTF8CString("Object"); JSObjectRef objectPrototype = (JSObjectRef)JSObjectGetProperty(jsContextRef, jsGlobalObjectRef, objectString, NULL); JSStringRelease(objectString); return JSValueIsInstanceOfConstructor(jsContextRef, jsObjectRef, objectPrototype, NULL); } @end NS_ASSUME_NONNULL_END TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END ================================================ FILE: Sources/App/Classes/Helpers/Plugin Architecture/THOPluginDispatcher.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClient.h" #import "IRCMessage.h" #import "THOPluginItemPrivate.h" #import "THOPluginManagerPrivate.h" #import "THOPluginProtocolPrivate.h" #import "THOPluginDispatcherPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const THOPluginProtocolCompatibilityMinimumVersion = @"7.2.4"; @interface IRCMessage (IRCMessagePluginExtension) - (THOPluginDidReceiveServerInputConcreteObject *)didReceiveServerInputConcreteObject; @end @implementation THOPluginDispatcher + (dispatch_queue_t)dispatchQueue { static dispatch_queue_t dispatchQueue = NULL; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ dispatchQueue = XRCreateDispatchQueueWithPriority("Textual.THOPluginDispatcher.PluginManagerDispatchQueue", DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT); }); return dispatchQueue; } + (BOOL)receivedCommand:(NSString *)command withText:(nullable NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt referenceMessage:(nullable IRCMessage *)referenceMessage { NSParameterAssert(command != nil); NSParameterAssert(client != nil); NSParameterAssert(receivedAt != nil); for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureDidReceiveCommandEvent] == NO) { continue; } BOOL returnedValue = [plugin.primaryClass receivedCommand:command withText:text authoredBy:textAuthor destinedFor:textDestination onClient:client receivedAt:receivedAt referenceMessage:referenceMessage]; if (returnedValue == NO) { return NO; } } return YES; } + (BOOL)receivedText:(NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination asLineType:(TVCLogLineType)lineType onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt wasEncrypted:(BOOL)wasEncrypted { NSParameterAssert(text != nil); NSParameterAssert(textAuthor != nil); NSParameterAssert(client != nil); NSParameterAssert(receivedAt != nil); for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent] == NO) { continue; } BOOL returnedValue = [plugin.primaryClass receivedText:text authoredBy:textAuthor destinedFor:textDestination asLineType:lineType onClient:client receivedAt:receivedAt wasEncrypted:wasEncrypted]; if (returnedValue == NO) { return NO; } } return YES; } + (nullable IRCMessage *)interceptServerInput:(IRCMessage *)inputObject for:(IRCClient *)client { NSParameterAssert(inputObject != nil); NSParameterAssert(client != nil); IRCMessage *returnValue = inputObject; for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureServerInputDataInterception] == NO) { continue; } IRCMessage *returnedValue = [plugin.primaryClass interceptServerInput:returnValue for:client]; if (returnedValue == nil) { return nil; } else if (returnedValue != returnValue) { if ([returnedValue isKindOfClass:[IRCMessageMutable class]]) { returnValue = [returnedValue copy]; } else { returnValue = returnedValue; } } } return returnValue; } + (nullable id)interceptUserInput:(id)inputObject command:(IRCRemoteCommand)commandString { NSParameterAssert(inputObject != nil); id returnValue = inputObject; for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureUserInputDataInterception] == NO) { continue; } id returnedValue = [plugin.primaryClass interceptUserInput:returnValue command:commandString]; if (returnedValue == nil) { return nil; } else if ([returnedValue isEqual:returnValue] == NO && ([returnedValue isKindOfClass:[NSString class]] || [returnedValue isKindOfClass:[NSAttributedString class]])) { if ([returnedValue isKindOfClass:[NSMutableString class]] || [returnedValue isKindOfClass:[NSMutableAttributedString class]]) { returnValue = [returnedValue copy]; } else { returnValue = returnedValue; } } } return returnValue; } + (NSString *)willRenderMessage:(NSString *)newMessage forViewController:(TVCLogController *)viewController lineType:(TVCLogLineType)lineType memberType:(TVCLogLineMemberType)memberType { NSParameterAssert(newMessage != nil); NSParameterAssert(viewController != nil); NSString *returnValue = newMessage; for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureWillRenderMessageEvent] == NO) { continue; } NSString *returnedValue = [plugin.primaryClass willRenderMessage:returnValue forViewController:viewController lineType:lineType memberType:memberType]; if (returnedValue.length == 0) { continue; } else if ([returnedValue isEqualToString:returnValue]) { continue; } returnValue = [returnedValue copy]; } return returnValue; } + (void)userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString { NSParameterAssert(client != nil); NSParameterAssert(commandString != nil); NSParameterAssert(messageString != nil); XRPerformBlockAsynchronouslyOnQueue([self dispatchQueue], ^{ NSString *lowercaseCommand = commandString.lowercaseString; NSString *uppercaseCommand = commandString.uppercaseString; for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureSubscribedUserInputCommands] == NO) { continue; } if ([plugin.supportedUserInputCommands containsObject:lowercaseCommand] == NO) { continue; } [plugin.primaryClass userInputCommandInvokedOnClient:client commandString:uppercaseCommand messageString:messageString]; } }); } + (void)didReceiveJavaScriptPayload:(THOPluginWebViewJavaScriptPayloadConcreteObject *)payloadObject fromViewController:(TVCLogController *)viewController { NSParameterAssert(payloadObject != nil); NSParameterAssert(viewController != nil); XRPerformBlockAsynchronouslyOnQueue([self dispatchQueue], ^{ for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureWebViewJavaScriptPayloads] == NO) { continue; } [plugin.primaryClass didReceiveJavaScriptPayload:payloadObject fromViewController:viewController]; } }); } + (void)didReceiveServerInput:(IRCMessage *)inputObject onClient:(IRCClient *)client { NSParameterAssert(inputObject != nil); NSParameterAssert(client != nil); XRPerformBlockAsynchronouslyOnQueue([self dispatchQueue], ^{ THOPluginDidReceiveServerInputConcreteObject *messageObject = inputObject.didReceiveServerInputConcreteObject; messageObject.networkAddress = client.serverAddress; messageObject.networkName = client.networkName; NSString *lowercaseCommand = inputObject.command.lowercaseString; for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureSubscribedServerInputCommands] == NO) { continue; } if ([plugin.supportedServerInputCommands containsObject:lowercaseCommand] == NO) { continue; } [plugin.primaryClass didReceiveServerInput:messageObject onClient:client]; } }); } + (NSCache *)didPostNewMessageObjectCache { static NSCache *queue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ queue = [NSCache new]; }); return queue; } + (void)enqueueDidPostNewMessage:(THOPluginDidPostNewMessageConcreteObject *)messageObject { NSParameterAssert(messageObject != nil); [[self didPostNewMessageObjectCache] setObject:messageObject forKey:messageObject.lineNumber]; } + (void)dequeueDidPostNewMessageWithLineNumber:(NSString *)messageLineNumber forViewController:(TVCLogController *)viewController { NSParameterAssert(messageLineNumber != nil); NSParameterAssert(viewController != nil); THOPluginDidPostNewMessageConcreteObject *messageObject = [[self didPostNewMessageObjectCache] objectForKey:messageLineNumber]; if (messageObject == nil) { return; } XRPerformBlockAsynchronouslyOnQueue([self dispatchQueue], ^{ for (THOPluginItem *plugin in sharedPluginManager().loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureNewMessagePostedEvent] == NO) { continue; } [plugin.primaryClass didPostNewMessage:messageObject forViewController:viewController]; } }); } @end #pragma mark - @implementation IRCMessage (IRCMessagePluginExtension) - (THOPluginDidReceiveServerInputConcreteObject *)didReceiveServerInputConcreteObject { THOPluginDidReceiveServerInputConcreteObject *messageObject = [THOPluginDidReceiveServerInputConcreteObject new]; messageObject.senderIsServer = self.senderIsServer; messageObject.senderNickname = self.senderNickname; messageObject.senderUsername = self.senderUsername; messageObject.senderAddress = self.senderAddress; messageObject.senderHostmask = self.senderHostmask; messageObject.receivedAt = self.receivedAt; messageObject.messageParameters = self.params; messageObject.messageParamaters = self.params; messageObject.messageSequence = self.sequence; messageObject.messageCommand = self.command; messageObject.messageCommandNumeric = self.commandNumeric; return messageObject; } @end #pragma mark - @implementation THOPluginDidPostNewMessageConcreteObject @end #pragma mark - @implementation THOPluginDidReceiveServerInputConcreteObject @end #pragma mark - @implementation THOPluginWebViewJavaScriptPayloadConcreteObject @end #pragma mark - @implementation THOPluginOutputSuppressionRule @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Helpers/Plugin Architecture/THOPluginItem.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "THOPluginProtocol.h" #import "THOPluginItemPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface THOPluginItem () @property (nonatomic, strong, readwrite, nullable) NSBundle *bundle; @property (nonatomic, strong, readwrite, nullable) id primaryClass; @property (nonatomic, assign, readwrite) THOPluginItemSupportedFeature supportedFeatures; @property (nonatomic, copy, readwrite, nullable) NSArray *supportedUserInputCommands; @property (nonatomic, copy, readwrite, nullable) NSArray *supportedServerInputCommands; @property (nonatomic, copy, readwrite, nullable) NSArray *outputSuppressionRules; @property (nonatomic, copy, readwrite, nullable) NSString *pluginPreferencesPaneMenuItemTitle; @property (nonatomic, strong, readwrite, nullable) NSView *pluginPreferencesPaneView; @end @implementation THOPluginItem #define _isClass(o, t) [o isKindOfClass:[t class]] #define _isNotEmptyArray(o) ([o isKindOfClass:[NSArray class]] && [(NSArray *)o count] > 0) #define _isNotEmptyString(o) ([o isKindOfClass:[NSString class]] && [(NSString *)o length] > 0) - (BOOL)loadBundle:(NSBundle *)bundle { NSParameterAssert(bundle != nil); /* Initialize the principal class */ Class principalClass = bundle.principalClass; if (principalClass == nil) { return NO; } id primaryClass = [[principalClass alloc] init]; if ([primaryClass respondsToSelector:@selector(pluginLoadedIntoMemory)]) { [primaryClass pluginLoadedIntoMemory]; } /* Build list of supported features */ THOPluginItemSupportedFeature supportedFeatures = 0; /* Process server output suppression rules */ if ([primaryClass respondsToSelector:@selector(pluginOutputSuppressionRules)]) { id outputRules = primaryClass.pluginOutputSuppressionRules; if (_isNotEmptyArray(outputRules)) { NSMutableArray *sharedRules = [NSMutableArray array]; for (id outputRule in outputRules) { if (_isClass(outputRule, THOPluginOutputSuppressionRule) == NO) { continue; } [sharedRules addObject:outputRule]; } self.outputSuppressionRules = sharedRules; supportedFeatures |= THOPluginItemSupportedFeatureOutputSuppressionRules; } } /* Does the bundle have a preference pane?... */ if ([primaryClass respondsToSelector:@selector(pluginPreferencesPaneMenuItemName)] && [primaryClass respondsToSelector:@selector(pluginPreferencesPaneView)]) { id itemTitle = primaryClass.pluginPreferencesPaneMenuItemName; id itemView = primaryClass.pluginPreferencesPaneView; if (_isNotEmptyString(itemTitle) && _isClass(itemView, NSView)) { self.pluginPreferencesPaneMenuItemTitle = itemTitle; self.pluginPreferencesPaneView = itemView; supportedFeatures |= THOPluginItemSupportedFeaturePreferencePane; } } /* Process user input commands */ if ([primaryClass respondsToSelector:@selector(subscribedUserInputCommands)] && [primaryClass respondsToSelector:@selector(userInputCommandInvokedOnClient:commandString:messageString:)]) { id subscribedCommands = primaryClass.subscribedUserInputCommands; if (_isNotEmptyArray(subscribedCommands)) { NSMutableArray *supportedCommands = [NSMutableArray array]; for (id command in subscribedCommands) { if (_isNotEmptyString(command) == NO) { continue; } [supportedCommands addObject:[command lowercaseString]]; } self.supportedUserInputCommands = supportedCommands; supportedFeatures |= THOPluginItemSupportedFeatureSubscribedUserInputCommands; } } /* Process server input commands */ if ([primaryClass respondsToSelector:@selector(subscribedServerInputCommands)] && [primaryClass respondsToSelector:@selector(didReceiveServerInput:onClient:)]) { id subscribedCommands = primaryClass.subscribedServerInputCommands; if (_isNotEmptyArray(subscribedCommands)) { NSMutableArray *supportedCommands = [NSMutableArray array]; for (id command in subscribedCommands) { if (_isNotEmptyString(command) == NO) { continue; } [supportedCommands addObject:[command lowercaseString]]; } self.supportedServerInputCommands = supportedCommands; supportedFeatures |= THOPluginItemSupportedFeatureSubscribedServerInputCommands; } } /* Check whether plugin supports certain events so we do not have to ask if it responds to the selector every time we call it. */ /* Renderer events */ if ([primaryClass respondsToSelector:@selector(didPostNewMessage:forViewController:)]) { supportedFeatures |= THOPluginItemSupportedFeatureNewMessagePostedEvent; } if ([primaryClass respondsToSelector:@selector(willRenderMessage:forViewController:lineType:memberType:)]) { supportedFeatures |= THOPluginItemSupportedFeatureWillRenderMessageEvent; } if ([primaryClass respondsToSelector:@selector(didReceiveJavaScriptPayload:fromViewController:)]) { supportedFeatures |= THOPluginItemSupportedFeatureWebViewJavaScriptPayloads; } /* Data interception */ if ([primaryClass respondsToSelector:@selector(interceptServerInput:for:)]) { supportedFeatures |= THOPluginItemSupportedFeatureServerInputDataInterception; } if ([primaryClass respondsToSelector:@selector(interceptUserInput:command:)]) { supportedFeatures |= THOPluginItemSupportedFeatureUserInputDataInterception; } if ([primaryClass respondsToSelector:@selector(receivedText:authoredBy:destinedFor:asLineType:onClient:receivedAt:wasEncrypted:)]) { supportedFeatures |= THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent; } if ([primaryClass respondsToSelector:@selector(receivedCommand:withText:authoredBy:destinedFor:onClient:receivedAt:referenceMessage:)]) { supportedFeatures |= THOPluginItemSupportedFeatureDidReceiveCommandEvent; } /* Finish up */ self.bundle = bundle; self.supportedFeatures = supportedFeatures; self.primaryClass = primaryClass; return YES; } - (void)unloadBundle { if (self.primaryClass == nil) { return; } if ([self.primaryClass respondsToSelector:@selector(pluginWillBeUnloadedFromMemory)]) { [self.primaryClass pluginWillBeUnloadedFromMemory]; } self.primaryClass = nil; self.bundle = nil; } - (BOOL)supportsFeature:(THOPluginItemSupportedFeature)feature { return ((self->_supportedFeatures & feature) == feature); } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Helpers/Plugin Architecture/THOPluginItemLogging.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2024 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "BuildConfig.h" #import "THOPluginManager.h" os_log_t _THOPluginLoggingSubsystemForBundle(NSBundle *bundle) { __block NSCache *subsystems = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ subsystems = [NSCache new]; }); @synchronized (subsystems) { NSString *identifier = bundle.bundleIdentifier; os_log_t subsystem = [subsystems objectForKey:identifier]; if (subsystem == nil) { NSString *category = [NSString stringWithFormat:@"Extension['%@']", bundle.displayName]; /* There was some debate whether to make the bundle identifier the identifier of the logging system instead of having the name in the category. Chose not to do that because seeing as the plugins are loaded as part of Textual and are not running in a separate process, it just makes more sense to filter. */ subsystem = os_log_create(TXBundleBuildProductIdentifierCString, category.UTF8String); [subsystems setObject:subsystem forKey:identifier]; } return subsystem; } } ================================================ FILE: Sources/App/Classes/Helpers/Plugin Architecture/THOPluginManager.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXGlobalModels.h" #import "TDCAlert.h" #import "TLOLocalization.h" #import "TPCApplicationInfo.h" #import "TPCPathInfo.h" #import "TPCPreferencesUserDefaults.h" #import "TPCResourceManager.h" #import "THOPluginDispatcherPrivate.h" #import "THOPluginItemPrivate.h" #import "THOPluginProtocol.h" #import "THOPluginManagerPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _extrasInstallerExtensionUpdateCheckInterval 345600 NSString * const THOPluginManagerFinishedLoadingPluginsNotification = @"THOPluginManagerFinishedLoadingPluginsNotification"; @interface THOPluginManager () @property (nonatomic, assign, readwrite) BOOL pluginsLoaded; @property (nonatomic, copy, readwrite, nullable) NSArray *loadedPlugins; @property (nonatomic, copy, nullable) NSArray *obsoleteBundles; @property (nonatomic, assign) THOPluginItemSupportedFeature supportedFeatures; @end @implementation THOPluginManager #pragma mark - #pragma mark Retain & Release - (void)loadPlugins { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ XRPerformBlockAsynchronouslyOnQueue([THOPluginDispatcher dispatchQueue], ^{ [self _loadPlugins]; }); }); } - (void)unloadPlugins { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ XRPerformBlockAsynchronouslyOnQueue([THOPluginDispatcher dispatchQueue], ^{ [self _unloadPlugins]; }); }); } - (void)_loadPlugins { NSArray *forbiddenPlugins = self.listOfForbiddenBundles; NSMutableArray *loadedPlugins = [NSMutableArray array]; NSMutableArray *bundlesToLoad = [NSMutableArray array]; NSMutableArray *loadedBundles = [NSMutableArray array]; NSMutableArray *obsoleteBundles = [NSMutableArray array]; NSArray *pathsToLoad = [RZFileManager() buildPathArray: [TPCPathInfo customExtensions], [TPCPathInfo bundledExtensions], nil]; for (NSString *path in pathsToLoad) { NSArray *pathFiles = [RZFileManager() contentsOfDirectoryAtPath:path error:NULL]; if (pathFiles == nil) { continue; } for (NSString *file in pathFiles) { if ([file hasSuffix:TPCResourceManagerBundleDocumentTypeExtension] == NO) { continue; } NSString *filePath = [path stringByAppendingPathComponent:file]; [bundlesToLoad addObject:filePath]; } } for (NSString *bundlePath in bundlesToLoad) { NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; if (bundle == nil) { continue; } NSString *bundleIdentifier = bundle.bundleIdentifier; if (bundleIdentifier == nil || [loadedBundles containsObject:bundleIdentifier]) { continue; } /* The list of forbidden bundles logic was added because a plugin previously bundled separately was not bundled with the app. This is a simple check to prevent the old plugin from loading and conflicting with built-in plugin. This is not designed as a security measure. */ if ([forbiddenPlugins containsObject:bundleIdentifier]) { LogToConsoleFault("Forbidden loading of plugin '%{public}@'", bundleIdentifier); continue; } /* Begin version comparison */ NSDictionary *infoDictionary = bundle.infoDictionary; NSString *comparisonVersion = infoDictionary[@"MinimumTextualVersion"]; if (comparisonVersion == nil) { [obsoleteBundles addObject:bundle]; NSLog(@" ---------------------------- ERROR ---------------------------- "); NSLog(@" "); NSLog(@" Textual has failed to load the bundle at the following path "); NSLog(@" which did not specify a minimum version: "); NSLog(@" "); NSLog(@" Bundle Path: %@", bundle.bundlePath); NSLog(@" "); NSLog(@" Please add a key-value pair in the bundle's Info.plist file "); NSLog(@" with the key name as \"MinimumTextualVersion\" "); NSLog(@" "); NSLog(@" For example, to support this version and later: "); NSLog(@" "); NSLog(@" MinimumTextualVersion "); NSLog(@" %@", THOPluginProtocolCompatibilityMinimumVersion); NSLog(@" "); NSLog(@" --------------------------------------------------------------- "); continue; } NSComparisonResult comparisonResult = [comparisonVersion compare:THOPluginProtocolCompatibilityMinimumVersion options:NSNumericSearch]; if (comparisonResult == NSOrderedAscending) { [obsoleteBundles addObject:bundle]; NSLog(@" ---------------------------- ERROR ---------------------------- "); NSLog(@" "); NSLog(@" Textual has failed to load the bundle at the following path "); NSLog(@" because the specified minimum version is out of range: "); NSLog(@" "); NSLog(@" Bundle Path: %@", bundle.bundlePath); NSLog(@" "); NSLog(@" Minimum version specified by bundle: %@", comparisonVersion); NSLog(@" Version used by Textual for comparison: %@", THOPluginProtocolCompatibilityMinimumVersion); NSLog(@" "); NSLog(@" --------------------------------------------------------------- "); continue; } /* Load bundle as a plugin */ THOPluginItem *plugin = [THOPluginItem new]; BOOL pluginLoaded = [plugin loadBundle:bundle]; if (pluginLoaded == NO) { continue; } [loadedPlugins addObject:plugin]; [loadedBundles addObject:bundleIdentifier]; [self updateSupportedFeaturesPropertyWithPlugin:plugin]; } self.loadedPlugins = loadedPlugins; self.obsoleteBundles = obsoleteBundles; self.pluginsLoaded = YES; XRPerformBlockAsynchronouslyOnMainQueue(^{ [self checkForObsoleteBundlesOrUpdatesAvailable]; [RZNotificationCenter() postNotificationName:THOPluginManagerFinishedLoadingPluginsNotification object:self]; }); } - (void)_unloadPlugins { for (THOPluginItem *plugin in self.loadedPlugins) { [plugin unloadBundle]; } self.loadedPlugins = nil; } #pragma mark - #pragma mark AppleScript Support - (NSArray *)supportedAppleScriptCommands { return [self supportedAppleScriptCommands:NO]; } - (NSDictionary *)supportedAppleScriptCommandsAndPaths { return [self supportedAppleScriptCommands:YES]; } - (id)supportedAppleScriptCommands:(BOOL)returnPathInfo { NSArray *forbiddenCommands = self.listOfForbiddenCommandNames; NSArray *scriptPaths = [RZFileManager() buildPathArray: [TPCPathInfo customScripts], [TPCPathInfo bundledScripts], nil]; id returnValue = nil; if (returnPathInfo) { returnValue = [NSMutableDictionary dictionary]; } else { returnValue = [NSMutableArray array]; } for (NSString *path in scriptPaths) { NSArray *pathFiles = [RZFileManager() contentsOfDirectoryAtPath:path error:NULL]; for (NSString *file in pathFiles) { NSString *filePath = [path stringByAppendingPathComponent:file]; NSString *fileExtension = file.pathExtension; NSString *fileWithoutExtension = file.stringByDeletingPathExtension; NSString *command = fileWithoutExtension.lowercaseString; BOOL executable = [RZFileManager() isExecutableFileAtPath:filePath]; if (executable == NO && [fileExtension isEqualToString:TPCResourceManagerScriptDocumentTypeExtensionWithoutPeriod] == NO) { LogToConsoleInfo("WARNING: File “%{public}@“ found in unsupervised script folder but it isn't AppleScript or an executable. It will be ignored.", file); continue; } else if ([forbiddenCommands containsObject:command]) { LogToConsoleInfo("WARNING: The command “%{public}@“ exists as a script file, but it is being ignored because the command name is forbidden.", fileWithoutExtension); continue; } if (returnPathInfo) { [returnValue setObjectWithoutOverride:filePath forKey:command]; } else { [returnValue addObjectWithoutDuplication:command]; } } } return returnValue; } - (NSArray *)listOfForbiddenCommandNames { /* List of commands that cannot be used as the name of a script because they would conflict with the commands defined by one or more standard (RFC) */ return [TPCResourceManager arrayFromResources:@"StaticStore" key:@"THOPluginManager List of Forbidden Commands"]; } - (NSArray *)listOfForbiddenBundles { /* List of bundle identifiers that are not allowed to load. */ return [TPCResourceManager arrayFromResources:@"StaticStore" key:@"THOPluginManager List of Forbidden Extensions"]; } #pragma mark - #pragma mark Extras Installer - (void)checkForObsoleteBundlesOrUpdatesAvailable { /* This method will perform three actions: 1. It will notify user if they have any 3rd-party obsolete addons. This will prompt them to contact the developer. 2. It will notify user if they have any obsolete extras installer addons that cannot be loaded. This will prompt to open installer. 3. It will notify the user if they have any extras installer addons that have an update available. #3 allows the user to suppress the prompt until a later time. #2 and #1 are aggressive. The prompt will show each launch until the addon is updated or deleted. It is possible that multiple prompts will appear on the screen at once. This will be considered an acceptable behavior for now. Just make sure non-blocking alerts are used for this purpose. */ [self checkForObsoleteBundles]; [self extrasInstallerCheckForUpdates]; } - (void)checkForObsoleteBundles { NSArray *obsoleteBundles = self.obsoleteBundles; if (obsoleteBundles.count == 0) { return; } NSMutableArray *obsoleteExtras = [NSMutableArray array]; NSMutableArray *obsoleteThirdParty = [NSMutableArray array]; NSArray *extrasBundleIdentifiers = self.extrasInstallerBundleIdentifiers; for (NSBundle *bundle in self.obsoleteBundles) { NSString *bundleIdentifier = bundle.bundleIdentifier; if ([extrasBundleIdentifiers containsObject:bundleIdentifier]) { [obsoleteExtras addObject:bundle]; } else { [obsoleteThirdParty addObject:bundle]; } } if (obsoleteExtras.count > 0) { [self _extrasInstallerInformUserAboutUpdateForBundles:[obsoleteExtras copy] updateOptional:NO]; } if (obsoleteThirdParty.count == 0) { return; } NSArray *thirdPartyBundles = [obsoleteThirdParty copy]; NSString *bundlesName = [NSBundle formattedDisplayNamesForBundles:thirdPartyBundles]; TVCAlert *alert = [TDCAlert alertWithMessage:TXTLS(@"Prompts[45a-df]", THOPluginProtocolCompatibilityMinimumVersion) title:TXTLS(@"Prompts[af6-45]", bundlesName) defaultButton:TXTLS(@"Prompts[324-5d]") alternateButton:nil otherButton:TXTLS(@"Prompts[0ik-o9]")]; [alert setButtonClickedBlock:^BOOL(TVCAlert *sender, TVCAlertResponseButton buttonClicked) { [NSBundle openInstallationLocationsForBundles:thirdPartyBundles]; return NO; } forButton:TVCAlertResponseButtonThird]; } - (void)extrasInstallerCheckForUpdates { /* Do not check for updates too often */ #define _defaultsKey @"THOPluginManager -> Extras Installer Last Check for Update Payload" NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; NSString *applicationVersion = [TPCApplicationInfo applicationVersion]; NSDictionary *lastUpdatePayload = [RZUserDefaults() dictionaryForKey:_defaultsKey]; if (lastUpdatePayload) { NSTimeInterval lastCheckTime = [lastUpdatePayload doubleForKey:@"lastCheck"]; NSString *lastVersion = [lastUpdatePayload stringForKey:@"lastVersion"]; if ((currentTime - lastCheckTime) < _extrasInstallerExtensionUpdateCheckInterval && [lastVersion isEqualToString:applicationVersion]) { return; } } /* Record the last time updates were checked for */ [RZUserDefaults() setObject:@{ @"lastCheck" : @(currentTime), @"lastVersion" : applicationVersion } forKey:_defaultsKey]; /* Check for updates */ [self _extrasInstallerCheckForUpdates]; #undef _defaultsKey } - (void)_extrasInstallerCheckForUpdates { /* Perform update check */ NSDictionary *latestVersions = self.extrasInstallerLatestBundleVersions; NSMutableArray *outdatedBundles = [NSMutableArray array]; for (THOPluginItem *plugin in self.loadedPlugins) { NSBundle *bundle = plugin.bundle; NSString *bundleIdentifier = bundle.bundleIdentifier; NSString *latestVersion = latestVersions[bundleIdentifier]; if (latestVersion == nil) { continue; } NSDictionary *infoDictionary = bundle.infoDictionary; NSString *currentVersion = infoDictionary[@"CFBundleVersion"]; NSComparisonResult comparisonResult = [currentVersion compare:latestVersion options:NSNumericSearch]; if (comparisonResult == NSOrderedAscending) { [outdatedBundles addObject:bundle]; } } if (outdatedBundles.count == 0) { return; } [self _extrasInstallerInformUserAboutUpdateForBundles:[outdatedBundles copy] updateOptional:YES]; } - (void)_extrasInstallerInformUserAboutUpdateForBundles:(NSArray *)bundles updateOptional:(BOOL)updateOptional { NSParameterAssert(bundles != nil); /* Append the current version to the suppression key so that updates aren't refused forever. Only until the next version of Textual is out. */ NSString *suppressionKey = nil; if (updateOptional) { suppressionKey = [@"plugin_manager_extension_update_dialog_" stringByAppendingString:[TPCApplicationInfo applicationVersionShort]]; } NSString *bundlesName = [NSBundle formattedDisplayNamesForBundles:bundles]; NSString *promptTitle = ((updateOptional) ? @"Prompts[9mb-o5]" : @"Prompts[ins-op]"); NSString *promptMessage = ((updateOptional) ? @"Prompts[x4w-is]" : @"Prompts[34o-pk]"); NSString *promptDefaultButton = ((updateOptional) ? @"Prompts[ece-dd]" : @"Prompts[hd0-bf]"); NSString *promptAlternateButton = ((updateOptional) ? @"Prompts[ioq-nf]" : @"Prompts[467-5l]"); NSString *promptOtherButton = ((updateOptional) ? nil : TXTLS(@"Prompts[h78-9e]")); TVCAlert *alert = [TDCAlert alertWithMessage:TXTLS(promptMessage) title:TXTLS(promptTitle, bundlesName) defaultButton:TXTLS(promptDefaultButton) alternateButton:TXTLS(promptAlternateButton) otherButton:promptOtherButton suppressionKey:suppressionKey suppressionText:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id _Nullable underlyingAlert) { if (buttonClicked == TDCAlertResponseAlternate) { [self extrasInstallerLaunchInstaller]; } }]; [alert setButtonClickedBlock:^BOOL(TVCAlert *sender, TVCAlertResponseButton buttonClicked) { [NSBundle openInstallationLocationsForBundles:bundles]; return NO; } forButton:TVCAlertResponseButtonThird]; } - (NSArray *)extrasInstallerBundleIdentifiers { return self.extrasInstallerLatestBundleVersions.allKeys; } - (NSDictionary *)extrasInstallerLatestBundleVersions { /* List of extra bundles and their latest version number. */ return [TPCResourceManager dictionaryFromResources:@"StaticStore" key:@"THOPluginManager Extras Installer Latest Extension Versions"]; } - (NSArray *)extrasInstallerReservedCommands { /* List of scripts that are available as downloadable content from the www.codeux.com website. */ return [TPCResourceManager arrayFromResources:@"StaticStore" key:@"THOPluginManager List of Reserved Commands"]; } - (void)findHandlerForOutgoingCommand:(NSString *)command path:(NSString * _Nullable *)path isReserved:(BOOL *)isReserved isScript:(BOOL *)isScript isExtension:(BOOL *)isExtension { NSParameterAssert(command != nil); /* Reset context pointers */ if ( path) { *path = nil; } if ( isReserved) { *isReserved = NO; } if ( isScript) { *isScript = NO; } if ( isExtension) { *isExtension = NO; } /* Find a script that matches this command */ NSDictionary *scriptPaths = self.supportedAppleScriptCommandsAndPaths; for (NSString *scriptCommand in scriptPaths) { if ([scriptCommand isEqualToString:command] == NO) { continue; } if ( path) { *path = scriptPaths[scriptCommand]; } if ( isScript) { *isScript = YES; } return; } /* Find an extension that matches this command */ BOOL pluginFound = [self.supportedUserInputCommands containsObject:command]; if (pluginFound) { if ( isExtension) { *isExtension = YES; } return; } /* Find a reserved command */ NSArray *reservedCommands = self.extrasInstallerReservedCommands; if ( isReserved) { *isReserved = [reservedCommands containsObject:command]; } } - (void)extrasInstallerAskUserIfTheyWantToInstallCommand:(NSString *)command { NSParameterAssert(command != nil); BOOL download = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[bpb-vv]") title:TXTLS(@"Prompts[o9p-4n]", command) defaultButton:TXTLS(@"Prompts[6lr-02]") alternateButton:TXTLS(@"Prompts[qso-2g]") suppressionKey:@"plugin_manager_reserved_command_dialog" suppressionText:nil]; if (download) { [self extrasInstallerLaunchInstaller]; } } - (void)extrasInstallerLaunchInstaller { NSURL *extrasURL = [RZMainBundle() URLForResource:@"Textual-Extras" withExtension:@"pkg"]; NSURL *installerURL = [RZWorkspace() URLForApplicationWithBundleIdentifier:@"com.apple.installer"]; [RZWorkspace() openURLs:@[extrasURL] withApplicationAtURL:installerURL configuration:[NSWorkspaceOpenConfiguration new] completionHandler:nil];; } #pragma mark - #pragma mark Extension Information - (void)updateSupportedFeaturesPropertyWithPlugin:(THOPluginItem *)plugin { NSParameterAssert(plugin != nil); #define _ef(_feature) if ([plugin supportsFeature:(_feature)] && [self supportsFeature:(_feature)] == NO) { \ self->_supportedFeatures |= (_feature); \ } _ef(THOPluginItemSupportedFeatureDidReceiveCommandEvent) _ef(THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent) // _ef(THOPluginItemSupportedFeatureInlineMediaManipulation) _ef(THOPluginItemSupportedFeatureNewMessagePostedEvent) _ef(THOPluginItemSupportedFeatureOutputSuppressionRules) _ef(THOPluginItemSupportedFeaturePreferencePane) _ef(THOPluginItemSupportedFeatureServerInputDataInterception) _ef(THOPluginItemSupportedFeatureSubscribedServerInputCommands) _ef(THOPluginItemSupportedFeatureSubscribedUserInputCommands) _ef(THOPluginItemSupportedFeatureUserInputDataInterception) _ef(THOPluginItemSupportedFeatureWebViewJavaScriptPayloads) _ef(THOPluginItemSupportedFeatureWillRenderMessageEvent) #undef _ef } - (BOOL)supportsFeature:(THOPluginItemSupportedFeature)feature { return ((self->_supportedFeatures & feature) == feature); } - (NSArray *)pluginOutputSuppressionRules { if (self.pluginsLoaded == NO) { return @[]; } static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMutableArray *allRules = [NSMutableArray array]; for (THOPluginItem *plugin in self.loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureOutputSuppressionRules] == NO) { continue; } NSArray *rules = plugin.outputSuppressionRules; if (rules) { [allRules addObjectsFromArray:rules]; } } cachedValue = [allRules copy]; }); return cachedValue; } - (NSArray *)supportedUserInputCommands { if (self.pluginsLoaded == NO) { return @[]; } static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMutableArray *allCommands = [NSMutableArray array]; for (THOPluginItem *plugin in self.loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureSubscribedUserInputCommands] == NO) { continue; } NSArray *commands = plugin.supportedUserInputCommands; for (NSString *command in commands) { [allCommands addObjectWithoutDuplication:command]; } } [allCommands sortUsingComparator:NSDefaultComparator]; cachedValue = [allCommands copy]; }); return cachedValue; } - (NSArray *)supportedServerInputCommands { if (self.pluginsLoaded == NO) { return @[]; } static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMutableArray *allCommands = [NSMutableArray array]; for (THOPluginItem *plugin in self.loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeatureSubscribedServerInputCommands] == NO) { continue; } NSArray *commands = plugin.supportedServerInputCommands; for (NSString *command in commands) { [allCommands addObjectWithoutDuplication:command]; } } [allCommands sortUsingComparator:NSDefaultComparator]; cachedValue = [allCommands copy]; }); return cachedValue; } - (NSArray *)pluginsWithPreferencePanes { if (self.pluginsLoaded == NO) { return @[]; } static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMutableArray *allExtensions = [NSMutableArray array]; for (THOPluginItem *plugin in self.loadedPlugins) { if ([plugin supportsFeature:THOPluginItemSupportedFeaturePreferencePane] == NO) { continue; } [allExtensions addObject:plugin]; } [allExtensions sortUsingComparator:^NSComparisonResult(THOPluginItem *object1, THOPluginItem *object2) { return [object1.pluginPreferencesPaneMenuItemTitle compare: object2.pluginPreferencesPaneMenuItemTitle]; }]; cachedValue = [allExtensions copy]; }); return cachedValue; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Helpers/THOUnicodeHelper.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "THOUnicodeHelper.h" NS_ASSUME_NONNULL_BEGIN static NSUInteger TABLE1[] = { 9, 0x00aa, 0x00aa, 0x00b5, 0x00b5, 0x00ba, 0x00ba, 0x00c0, 0x00d6, 0x00d8, 0x00f6, 0x00f8, 0x02c1, 0x02c6, 0x02d1, 0x02e0, 0x02e4, 0x02ee, 0x02ee, }; static NSUInteger TABLE2[] = { 134, 0x037a, 0x037d, 0x0386, 0x0386, 0x0388, 0x038a, 0x038c, 0x038c, 0x038e, 0x03a1, 0x03a3, 0x03ce, 0x03d0, 0x03f5, 0x03f7, 0x0481, 0x048a, 0x0513, 0x0531, 0x0556, 0x0559, 0x0559, 0x0561, 0x0587, 0x05d0, 0x05ea, 0x05f0, 0x05f2, 0x0621, 0x063a, 0x0640, 0x064a, 0x066e, 0x066f, 0x0671, 0x06d3, 0x06d5, 0x06d5, 0x06e5, 0x06e6, 0x06ee, 0x06ef, 0x06fa, 0x06fc, 0x06ff, 0x06ff, 0x0710, 0x0710, 0x0712, 0x072f, 0x074d, 0x076d, 0x0780, 0x07a5, 0x07b1, 0x07b1, 0x07ca, 0x07ea, 0x07f4, 0x07f5, 0x07fa, 0x07fa, 0x0904, 0x0939, 0x093d, 0x093d, 0x0950, 0x0950, 0x0958, 0x0961, 0x097b, 0x097f, 0x0985, 0x098c, 0x098f, 0x0990, 0x0993, 0x09a8, 0x09aa, 0x09b0, 0x09b2, 0x09b2, 0x09b6, 0x09b9, 0x09bd, 0x09bd, 0x09ce, 0x09ce, 0x09dc, 0x09dd, 0x09df, 0x09e1, 0x09f0, 0x09f1, 0x0a05, 0x0a0a, 0x0a0f, 0x0a10, 0x0a13, 0x0a28, 0x0a2a, 0x0a30, 0x0a32, 0x0a33, 0x0a35, 0x0a36, 0x0a38, 0x0a39, 0x0a59, 0x0a5c, 0x0a5e, 0x0a5e, 0x0a72, 0x0a74, 0x0a85, 0x0a8d, 0x0a8f, 0x0a91, 0x0a93, 0x0aa8, 0x0aaa, 0x0ab0, 0x0ab2, 0x0ab3, 0x0ab5, 0x0ab9, 0x0abd, 0x0abd, 0x0ad0, 0x0ad0, 0x0ae0, 0x0ae1, 0x0b05, 0x0b0c, 0x0b0f, 0x0b10, 0x0b13, 0x0b28, 0x0b2a, 0x0b30, 0x0b32, 0x0b33, 0x0b35, 0x0b39, 0x0b3d, 0x0b3d, 0x0b5c, 0x0b5d, 0x0b5f, 0x0b61, 0x0b71, 0x0b71, 0x0b83, 0x0b83, 0x0b85, 0x0b8a, 0x0b8e, 0x0b90, 0x0b92, 0x0b95, 0x0b99, 0x0b9a, 0x0b9c, 0x0b9c, 0x0b9e, 0x0b9f, 0x0ba3, 0x0ba4, 0x0ba8, 0x0baa, 0x0bae, 0x0bb9, 0x0c05, 0x0c0c, 0x0c0e, 0x0c10, 0x0c12, 0x0c28, 0x0c2a, 0x0c33, 0x0c35, 0x0c39, 0x0c60, 0x0c61, 0x0c85, 0x0c8c, 0x0c8e, 0x0c90, 0x0c92, 0x0ca8, 0x0caa, 0x0cb3, 0x0cb5, 0x0cb9, 0x0cbd, 0x0cbd, 0x0cde, 0x0cde, 0x0ce0, 0x0ce1, 0x0d05, 0x0d0c, 0x0d0e, 0x0d10, 0x0d12, 0x0d28, 0x0d2a, 0x0d39, 0x0d60, 0x0d61, 0x0d85, 0x0d96, 0x0d9a, 0x0db1, 0x0db3, 0x0dbb, 0x0dbd, 0x0dbd, 0x0dc0, 0x0dc6, 0x0e01, 0x0e30, 0x0e32, 0x0e33, 0x0e40, 0x0e46, 0x0e81, 0x0e82, 0x0e84, 0x0e84, 0x0e87, 0x0e88, 0x0e8a, 0x0e8a, 0x0e8d, 0x0e8d, 0x0e94, 0x0e97, 0x0e99, 0x0e9f, 0x0ea1, 0x0ea3, 0x0ea5, 0x0ea5, 0x0ea7, 0x0ea7, 0x0eaa, 0x0eab, 0x0ead, 0x0eb0, 0x0eb2, 0x0eb3, 0x0ebd, 0x0ebd, 0x0ec0, 0x0ec4, 0x0ec6, 0x0ec6, 0x0edc, 0x0edd, 0x0f00, 0x0f00, 0x0f40, 0x0f47, 0x0f49, 0x0f6a, 0x0f88, 0x0f8b, }; static NSUInteger TABLE3[] = { 70, 0x1000, 0x1021, 0x1023, 0x1027, 0x1029, 0x102a, 0x1050, 0x1055, 0x10a0, 0x10c5, 0x10d0, 0x10fa, 0x10fc, 0x10fc, 0x1200, 0x1248, 0x124a, 0x124d, 0x1250, 0x1256, 0x1258, 0x1258, 0x125a, 0x125d, 0x1260, 0x1288, 0x128a, 0x128d, 0x1290, 0x12b0, 0x12b2, 0x12b5, 0x12b8, 0x12be, 0x12c0, 0x12c0, 0x12c2, 0x12c5, 0x12c8, 0x12d6, 0x12d8, 0x1310, 0x1312, 0x1315, 0x1318, 0x135a, 0x1380, 0x138f, 0x13a0, 0x13f4, 0x1401, 0x166c, 0x166f, 0x1676, 0x1681, 0x169a, 0x16a0, 0x16ea, 0x1700, 0x170c, 0x170e, 0x1711, 0x1720, 0x1731, 0x1740, 0x1751, 0x1760, 0x176c, 0x176e, 0x1770, 0x1780, 0x17b3, 0x17d7, 0x17d7, 0x17dc, 0x17dc, 0x1820, 0x1877, 0x1880, 0x18a8, 0x1900, 0x191c, 0x1950, 0x196d, 0x1970, 0x1974, 0x1980, 0x19a9, 0x19c1, 0x19c7, 0x1a00, 0x1a16, 0x1b05, 0x1b33, 0x1b45, 0x1b4b, 0x1d00, 0x1dbf, 0x1e00, 0x1e9b, 0x1ea0, 0x1ef9, 0x1f00, 0x1f15, 0x1f18, 0x1f1d, 0x1f20, 0x1f45, 0x1f48, 0x1f4d, 0x1f50, 0x1f57, 0x1f59, 0x1f59, 0x1f5b, 0x1f5b, 0x1f5d, 0x1f5d, 0x1f5f, 0x1f7d, 0x1f80, 0x1fb4, 0x1fb6, 0x1fbc, 0x1fbe, 0x1fbe, 0x1fc2, 0x1fc4, 0x1fc6, 0x1fcc, 0x1fd0, 0x1fd3, 0x1fd6, 0x1fdb, 0x1fe0, 0x1fec, 0x1ff2, 0x1ff4, 0x1ff6, 0x1ffc, }; static NSUInteger TABLE4[] = { 34, 0x2071, 0x2071, 0x207f, 0x207f, 0x2090, 0x2094, 0x2102, 0x2102, 0x2107, 0x2107, 0x210a, 0x2113, 0x2115, 0x2115, 0x2119, 0x211d, 0x2124, 0x2124, 0x2126, 0x2126, 0x2128, 0x2128, 0x212a, 0x212d, 0x212f, 0x2139, 0x213c, 0x213f, 0x2145, 0x2149, 0x214e, 0x214e, 0x2183, 0x2184, 0x2c00, 0x2c2e, 0x2c30, 0x2c5e, 0x2c60, 0x2c6c, 0x2c74, 0x2c77, 0x2c80, 0x2ce4, 0x2d00, 0x2d25, 0x2d30, 0x2d65, 0x2d6f, 0x2d6f, 0x2d80, 0x2d96, 0x2da0, 0x2da6, 0x2da8, 0x2dae, 0x2db0, 0x2db6, 0x2db8, 0x2dbe, 0x2dc0, 0x2dc6, 0x2dc8, 0x2dce, 0x2dd0, 0x2dd6, 0x2dd8, 0x2dde, }; static NSUInteger TABLE5[] = { 7, 0xa000, 0xa48c, 0xa717, 0xa71a, 0xa800, 0xa801, 0xa803, 0xa805, 0xa807, 0xa80a, 0xa80c, 0xa822, 0xa840, 0xa873, }; static NSUInteger TABLE6[] = { 18, 0xfb00, 0xfb06, 0xfb13, 0xfb17, 0xfb1d, 0xfb1d, 0xfb1f, 0xfb28, 0xfb2a, 0xfb36, 0xfb38, 0xfb3c, 0xfb3e, 0xfb3e, 0xfb40, 0xfb41, 0xfb43, 0xfb44, 0xfb46, 0xfbb1, 0xfbd3, 0xfd3d, 0xfd50, 0xfd8f, 0xfd92, 0xfdc7, 0xfdf0, 0xfdfb, 0xfe70, 0xfe74, 0xfe76, 0xfefc, 0xff21, 0xff3a, 0xff41, 0xff5a, }; static NSUInteger OTHERS_TABLE[] = { 56, 0x10000, 0x1000b, 0x1000d, 0x10026, 0x10028, 0x1003a, 0x1003c, 0x1003d, 0x1003f, 0x1004d, 0x10050, 0x1005d, 0x10080, 0x100fa, 0x10300, 0x1031e, 0x10330, 0x10340, 0x10342, 0x10349, 0x10380, 0x1039d, 0x103a0, 0x103c3, 0x103c8, 0x103cf, 0x10400, 0x1049d, 0x10800, 0x10805, 0x10808, 0x10808, 0x1080a, 0x10835, 0x10837, 0x10838, 0x1083c, 0x1083c, 0x1083f, 0x1083f, 0x10900, 0x10915, 0x10a00, 0x10a00, 0x10a10, 0x10a13, 0x10a15, 0x10a17, 0x10a19, 0x10a33, 0x12000, 0x1236e, 0x1d400, 0x1d454, 0x1d456, 0x1d49c, 0x1d49e, 0x1d49f, 0x1d4a2, 0x1d4a2, 0x1d4a5, 0x1d4a6, 0x1d4a9, 0x1d4ac, 0x1d4ae, 0x1d4b9, 0x1d4bb, 0x1d4bb, 0x1d4bd, 0x1d4c3, 0x1d4c5, 0x1d505, 0x1d507, 0x1d50a, 0x1d50d, 0x1d514, 0x1d516, 0x1d51c, 0x1d51e, 0x1d539, 0x1d53b, 0x1d53e, 0x1d540, 0x1d544, 0x1d546, 0x1d546, 0x1d54a, 0x1d550, 0x1d552, 0x1d6a5, 0x1d6a8, 0x1d6c0, 0x1d6c2, 0x1d6da, 0x1d6dc, 0x1d6fa, 0x1d6fc, 0x1d714, 0x1d716, 0x1d734, 0x1d736, 0x1d74e, 0x1d750, 0x1d76e, 0x1d770, 0x1d788, 0x1d78a, 0x1d7a8, 0x1d7aa, 0x1d7c2, 0x1d7c4, 0x1d7cb, }; @implementation THOUnicodeHelper + (BOOL)isPrivate:(UniChar)c { return (0xe000 <= c && c <= 0xf8ff); } + (BOOL)isIdeographic:(UniChar)c { return ((0x2e80 <= c && c <= 0x9fff) || (0xa000 <= c && c <= 0xa4cf) || (0xf900 <= c && c <= 0xfaff) || (0xfe30 <= c && c <= 0xfe4f) || (0xff00 <= c && c <= 0xffef)); } + (BOOL)isIdeographicOrPrivate:(UniChar)c { return ((0x2e80 <= c && c <= 0x9fff) || (0xa000 <= c && c <= 0xa4cf) || (0xe000 <= c && c <= 0xfaff) || (0xfe30 <= c && c <= 0xfe4f) || (0xff00 <= c && c <= 0xffef)); } + (BOOL)isAlphabeticalCodePoint:(NSInteger)c { NSUInteger *T = 0; if (c <= 0x7f) { return ((0x41 <= c && c <= 0x5a) || (0x61 <= c && c <= 0x7a)); } if (0xaa <= c && c <= 0x2ee) { T = TABLE1; } else if (0x37a <= c && c <= 0xf8b) { T = TABLE2; } else if (0x1000 <= c && c <= 0x1ffc) { T = TABLE3; } else if (0x2071 <= c && c <= 0x2dde) { T = TABLE4; } else if (0xa000 <= c && c <= 0xa873) { T = TABLE5; } else if (0xfb00 <= c && c <= 0xff5a) { T = TABLE6; } else if (0x10000 <= c && c <= 0x1d7cb) { T = OTHERS_TABLE; } if (T == 0) { return NO; } NSInteger count = *T; T++; NSInteger left = 0; NSInteger right = count; while (left < right) { NSInteger center = ((left + right) / 2); NSInteger start = T[(center * 2)]; NSInteger end = T[(center * 2 + 1)]; if (start <= c && c <= end) { return YES; } if (c < start) { right = center; continue; } else { left = (center + 1); continue; } } return NO; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCAddressBook.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual and/or Codeux Software, nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSStringHelper.h" #import "IRCAddressBookInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCAddressBookEntry - (void)populateDefaultsPreflight { if (self.initializedAsCopy) { return; } self->_defaults = @{ @"entryType" : @(IRCAddressBookEntryTypeIgnore), @"ignoreClientToClientProtocol" : @(NO), @"ignoreFileTransferRequests" : @(NO), @"ignoreGeneralEventMessages" : @(NO), @"ignoreInlineMedia" : @(NO), @"ignoreNoticeMessages" : @(NO), @"ignorePrivateMessageHighlights" : @(NO), @"ignorePrivateMessages" : @(NO), @"ignorePublicMessageHighlights" : @(NO), @"ignorePublicMessages" : @(NO), @"trackUserActivity" : @(NO) }; } - (void)populateDefaultsPostflight { if (self.initializedAsCopy) { return; } SetVariableIfNil(self->_hostmask, @"") SetVariableIfNil(self->_hostmaskRegularExpression, @"") SetVariableIfNil(self->_uniqueIdentifier, [NSString stringWithUUID]) } - (void)initializedClassHealthCheck { if (self.initializedAsCopy) { return; } [self rebuildCache]; } + (instancetype)newIgnoreEntry { return [self newIgnoreEntryForHostmask:nil]; } + (instancetype)newIgnoreEntryForHostmask:(nullable NSString *)hostmask { if (hostmask == nil) { hostmask = @""; } NSDictionary *dic = @{ @"hostmask" : hostmask, @"entryType" : @(IRCAddressBookEntryTypeIgnore), @"ignoreClientToClientProtocol" : @(YES), @"ignoreFileTransferRequests" : @(YES), @"ignoreGeneralEventMessages" : @(YES), @"ignoreInlineMedia" : @(YES), @"ignoreNoticeMessages" : @(YES), @"ignorePrivateMessageHighlights" : @(YES), @"ignorePrivateMessages" : @(YES), @"ignorePublicMessageHighlights" : @(YES), @"ignorePublicMessages" : @(YES) }; IRCAddressBookEntry *object = [[self alloc] initWithDictionary:dic]; return object; } + (instancetype)newUserTrackingEntry { NSDictionary *dic = @{ @"entryType" : @(IRCAddressBookEntryTypeUserTracking), @"trackUserActivity" : @(YES) }; IRCAddressBookEntry *object = [[self alloc] initWithDictionary:dic]; return object; } - (void)populateDictionaryValues:(NSDictionary *)dic { NSParameterAssert(dic != nil); NSMutableDictionary *defaultsMutable = [self->_defaults mutableCopy]; [defaultsMutable addEntriesFromDictionary:dic]; [dic assignUnsignedIntegerTo:&self->_entryType forKey:@"entryType"]; IRCAddressBookEntryType entryType = self->_entryType; if (entryType == IRCAddressBookEntryTypeIgnore || entryType == IRCAddressBookEntryTypeMixed) { /* Load the newest set of keys */ [dic assignBoolTo:&self->_ignoreClientToClientProtocol forKey:@"ignoreClientToClientProtocol"]; [dic assignBoolTo:&self->_ignoreFileTransferRequests forKey:@"ignoreFileTransferRequests"]; [dic assignBoolTo:&self->_ignoreGeneralEventMessages forKey:@"ignoreGeneralEventMessages"]; [dic assignBoolTo:&self->_ignoreInlineMedia forKey:@"ignoreInlineMedia"]; [dic assignBoolTo:&self->_ignoreNoticeMessages forKey:@"ignoreNoticeMessages"]; [dic assignBoolTo:&self->_ignorePrivateMessageHighlights forKey:@"ignorePrivateMessageHighlights"]; [dic assignBoolTo:&self->_ignorePrivateMessages forKey:@"ignorePrivateMessages"]; [dic assignBoolTo:&self->_ignorePublicMessageHighlights forKey:@"ignorePublicMessageHighlights"]; [dic assignBoolTo:&self->_ignorePublicMessages forKey:@"ignorePublicMessages"]; /* Load legacy keys (if they exist) */ [dic assignBoolTo:&self->_ignoreClientToClientProtocol forKey:@"ignoreCTCP"]; [dic assignBoolTo:&self->_ignoreGeneralEventMessages forKey:@"ignoreJPQE"]; [dic assignBoolTo:&self->_ignoreNoticeMessages forKey:@"ignoreNotices"]; [dic assignBoolTo:&self->_ignorePrivateMessageHighlights forKey:@"ignorePMHighlights"]; [dic assignBoolTo:&self->_ignorePrivateMessages forKey:@"ignorePrivateMsg"]; [dic assignBoolTo:&self->_ignorePublicMessageHighlights forKey:@"ignoreHighlights"]; [dic assignBoolTo:&self->_ignorePublicMessages forKey:@"ignorePublicMsg"]; } if (entryType == IRCAddressBookEntryTypeUserTracking || entryType == IRCAddressBookEntryTypeMixed) { /* Load the newest set of keys */ [dic assignBoolTo:&self->_trackUserActivity forKey:@"trackUserActivity"]; /* Load legacy keys (if they exist) */ [dic assignBoolTo:&self->_trackUserActivity forKey:@"notifyJoins"]; } [dic assignStringTo:&self->_hostmask forKey:@"hostmask"]; [dic assignStringTo:&self->_uniqueIdentifier forKey:@"uniqueIdentifier"]; } - (void)rebuildCache { [self rebuildHostmaskRegularExpression]; [self rebuildTrackingNickname]; } - (void)rebuildHostmaskRegularExpression { NSString *hostmask = self.hostmask; if (self.entryType == IRCAddressBookEntryTypeIgnore) { hostmask = [hostmask stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"{" withString:@"\\{"]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"}" withString:@"\\}"]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@")" withString:@"\\)"]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"(" withString:@"\\("]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"]" withString:@"\\]"]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"[" withString:@"\\["]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"^" withString:@"\\^"]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"|" withString:@"\\|"]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"~" withString:@"\\~"]; hostmask = [hostmask stringByReplacingOccurrencesOfString:@"*" withString:@"(.*?)"]; } else if (self.entryType == IRCAddressBookEntryTypeUserTracking) { hostmask = [NSString stringWithFormat:@"^%@!(.*?)@(.*?)$", hostmask]; } else { return; } self->_hostmaskRegularExpression = [hostmask copy]; } - (void)rebuildTrackingNickname { if (self.entryType != IRCAddressBookEntryTypeUserTracking) { return; } NSString *hostmask = self.hostmask; hostmask = hostmask.nicknameFromHostmask; self->_trackingNickname = [hostmask copy]; } - (BOOL)checkMatch:(NSString *)hostmask { NSParameterAssert(hostmask != nil); return [XRRegularExpression string:hostmask isMatchedByRegex:self.hostmaskRegularExpression withoutCase:YES]; } - (NSDictionary *)dictionaryValueForTarget:(XRPortablePropertyDictTarget)target { NSMutableDictionary *dic = [NSMutableDictionary dictionary]; [dic maybeSetObject:self.hostmask forKey:@"hostmask"]; [dic maybeSetObject:self.uniqueIdentifier forKey:@"uniqueIdentifier"]; IRCAddressBookEntryType entryType = self.entryType; if (entryType == IRCAddressBookEntryTypeIgnore || entryType == IRCAddressBookEntryTypeMixed) { [dic setBool:self.ignoreClientToClientProtocol forKey:@"ignoreClientToClientProtocol"]; [dic setBool:self.ignoreFileTransferRequests forKey:@"ignoreFileTransferRequests"]; [dic setBool:self.ignoreGeneralEventMessages forKey:@"ignoreGeneralEventMessages"]; [dic setBool:self.ignoreInlineMedia forKey:@"ignoreInlineMedia"]; [dic setBool:self.ignoreNoticeMessages forKey:@"ignoreNoticeMessages"]; [dic setBool:self.ignorePrivateMessageHighlights forKey:@"ignorePrivateMessageHighlights"]; [dic setBool:self.ignorePrivateMessages forKey:@"ignorePrivateMessages"]; [dic setBool:self.ignorePublicMessageHighlights forKey:@"ignorePublicMessageHighlights"]; [dic setBool:self.ignorePublicMessages forKey:@"ignorePublicMessages"]; } if (entryType == IRCAddressBookEntryTypeUserTracking || entryType == IRCAddressBookEntryTypeMixed) { [dic setBool:self.trackUserActivity forKey:@"trackUserActivity"]; } [dic setUnsignedInteger:entryType forKey:@"entryType"]; if (target == XRPortablePropertyDictTargetCopy || target == XRPortablePropertyDictTargetMutableCopy) { return [dic copy]; } return [dic dictionaryByRemovingDefaults:self->_defaults]; } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { IRCAddressBookEntry *config = [self allocForCopyAsMutable:mutableCopy]; config->_defaults = self->_defaults; config->_hostmaskRegularExpression = self->_hostmaskRegularExpression; config->_trackingNickname = self->_trackingNickname; config->_parentEntries = self->_parentEntries; if (uniquing) { config->_uniqueIdentifier = [NSString stringWithUUID]; } return [config initWithDictionary:self.dictionaryValueForCopy]; } - (__kindof XRPortablePropertyDict *)mutableClass { return [IRCAddressBookEntryMutable self]; } @end #pragma mark - @implementation IRCAddressBookEntryMutable @dynamic entryType; @dynamic hostmask; @dynamic ignoreClientToClientProtocol; @dynamic ignoreFileTransferRequests; @dynamic ignoreGeneralEventMessages; @dynamic ignoreInlineMedia; @dynamic ignoreNoticeMessages; @dynamic ignorePrivateMessageHighlights; @dynamic ignorePrivateMessages; @dynamic ignorePublicMessageHighlights; @dynamic ignorePublicMessages; @dynamic trackUserActivity; @dynamic parentEntries; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyDict *)immutableClass { return [IRCAddressBookEntry self]; } - (void)setEntryType:(IRCAddressBookEntryType)entryType { if (self->_entryType != entryType) { self->_entryType = entryType; [self rebuildCache]; } } - (void)setHostmask:(NSString *)hostmask { NSParameterAssert(hostmask != nil); if (self->_hostmask != hostmask) { self->_hostmask = [hostmask copy]; [self rebuildCache]; } } - (void)setIgnoreClientToClientProtocol:(BOOL)ignoreClientToClientProtocol { if (self->_ignoreClientToClientProtocol != ignoreClientToClientProtocol) { self->_ignoreClientToClientProtocol = ignoreClientToClientProtocol; } } - (void)setIgnoreFileTransferRequests:(BOOL)ignoreFileTransferRequests { if (self->_ignoreFileTransferRequests != ignoreFileTransferRequests) { self->_ignoreFileTransferRequests = ignoreFileTransferRequests; } } - (void)setIgnoreGeneralEventMessages:(BOOL)ignoreGeneralEventMessages { if (self->_ignoreGeneralEventMessages != ignoreGeneralEventMessages) { self->_ignoreGeneralEventMessages = ignoreGeneralEventMessages; } } - (void)setIgnoreInlineMedia:(BOOL)ignoreInlineMedia { if (self->_ignoreInlineMedia != ignoreInlineMedia) { self->_ignoreInlineMedia = ignoreInlineMedia; } } - (void)setIgnoreNoticeMessages:(BOOL)ignoreNoticeMessages { if (self->_ignoreNoticeMessages != ignoreNoticeMessages) { self->_ignoreNoticeMessages = ignoreNoticeMessages; } } - (void)setIgnorePrivateMessageHighlights:(BOOL)ignorePrivateMessageHighlights { if (self->_ignorePrivateMessageHighlights != ignorePrivateMessageHighlights) { self->_ignorePrivateMessageHighlights = ignorePrivateMessageHighlights; } } - (void)setIgnorePrivateMessages:(BOOL)ignorePrivateMessages { if (self->_ignorePrivateMessages != ignorePrivateMessages) { self->_ignorePrivateMessages = ignorePrivateMessages; } } - (void)setIgnorePublicMessageHighlights:(BOOL)ignorePublicMessageHighlights { if (self->_ignorePublicMessageHighlights != ignorePublicMessageHighlights) { self->_ignorePublicMessageHighlights = ignorePublicMessageHighlights; } } - (void)setIgnorePublicMessages:(BOOL)ignorePublicMessages { if (self->_ignorePublicMessages != ignorePublicMessages) { self->_ignorePublicMessages = ignorePublicMessages; } } - (void)setTrackUserActivity:(BOOL)trackUserActivity { if (self->_trackUserActivity != trackUserActivity) { self->_trackUserActivity = trackUserActivity; } } - (void)setParentEntries:(nullable NSArray *)childrenEntries { if (self->_parentEntries != childrenEntries) { self->_parentEntries = [childrenEntries copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCAddressBookMatchCache.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClient.h" #import "IRCClientConfig.h" #import "IRCAddressBook.h" #import "IRCAddressBookMatchCachePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCAddressBookMatchCache () @property (nonatomic, weak) IRCClient *client; @property (nonatomic, strong) NSCache *cachedMatchesInt; @end @implementation IRCAddressBookMatchCache - (instancetype)initWithClient:(IRCClient *)client { NSParameterAssert(client != nil); if ((self = [super init])) { self.client = client; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { NSCache *cachedMatches = [NSCache new]; cachedMatches.countLimit = 100; self.cachedMatchesInt = cachedMatches; } - (void)clearCachedMatches { [self.cachedMatchesInt removeAllObjects]; } - (void)clearCachedMatchesForHostmask:(NSString *)hostmask { NSParameterAssert(hostmask != nil); [self.cachedMatchesInt removeObjectForKey:hostmask]; } - (NSArray *)findIgnoresForHostmask:(NSString *)hostmask { IRCAddressBookEntry *match = [self findAddressBookEntryForHostmask:hostmask]; if (match && match.entryType == IRCAddressBookEntryTypeIgnore) { return @[match]; } if (match && match.entryType == IRCAddressBookEntryTypeMixed) { NSArray *parentEntries = match.parentEntries; return [parentEntries filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"entryType == %d", IRCAddressBookEntryTypeIgnore]]; } return @[]; } - (nullable IRCAddressBookEntry *)findAddressBookEntryForHostmask:(NSString *)hostmask { NSParameterAssert(hostmask != nil); IRCAddressBookEntry *cachedEntry = [self.cachedMatchesInt objectForKey:hostmask]; if (cachedEntry) { /* We reset the cache when new items are added to the client which means we are perfectly fine keeping a cache of when there isn't result for a hostmask. Whatever is needed to gain a little bit more speed. */ if ([cachedEntry isKindOfClass:[NSNull class]]) { return nil; } return cachedEntry; } /* A separate variable is used to store a single entry and multiple so that we don’t allocate an array when it is very infrequent for there to be multiple. */ IRCAddressBookEntry *matchedEntry = nil; NSMutableArray *matchedEntries = nil; for (IRCAddressBookEntry *entry in self.client.config.ignoreList) { if ([entry checkMatch:hostmask] == NO) { continue; } if (matchedEntries) { [matchedEntries addObject:entry]; } else if (matchedEntry) { matchedEntries = [NSMutableArray array]; [matchedEntries addObject:matchedEntry]; [matchedEntries addObject:entry]; matchedEntry = nil; } else { matchedEntry = entry; } } /* Combine multiple entries */ if (matchedEntries) { IRCAddressBookEntryMutable *mixedEntry = [IRCAddressBookEntryMutable new]; mixedEntry.entryType = IRCAddressBookEntryTypeMixed; mixedEntry.parentEntries = matchedEntries; for (IRCAddressBookEntry *entry in matchedEntries) { mixedEntry.ignoreClientToClientProtocol = ((entry.ignoreClientToClientProtocol) ? YES : mixedEntry.ignoreClientToClientProtocol); mixedEntry.ignoreGeneralEventMessages = ((entry.ignoreGeneralEventMessages) ? YES : mixedEntry.ignoreGeneralEventMessages); mixedEntry.ignoreNoticeMessages = ((entry.ignoreNoticeMessages) ? YES : mixedEntry.ignoreNoticeMessages); mixedEntry.ignorePrivateMessageHighlights = ((entry.ignorePrivateMessageHighlights) ? YES : mixedEntry.ignorePrivateMessageHighlights); mixedEntry.ignorePrivateMessages = ((entry.ignorePrivateMessages) ? YES : mixedEntry.ignorePrivateMessages); mixedEntry.ignorePublicMessageHighlights = ((entry.ignorePublicMessageHighlights) ? YES : mixedEntry.ignorePublicMessageHighlights); mixedEntry.ignorePublicMessages = ((entry.ignorePublicMessages) ? YES : mixedEntry.ignorePublicMessages); mixedEntry.ignoreFileTransferRequests = ((entry.ignoreFileTransferRequests) ? YES : mixedEntry.ignoreFileTransferRequests); mixedEntry.ignoreInlineMedia = ((entry.ignoreInlineMedia) ? YES : mixedEntry.ignoreInlineMedia); mixedEntry.trackUserActivity = ((entry.trackUserActivity) ? YES : mixedEntry.trackUserActivity); } matchedEntry = [mixedEntry copy]; } /* Cache entry */ if (matchedEntry) { [self.cachedMatchesInt setObject:matchedEntry forKey:hostmask]; } else { [self.cachedMatchesInt setObject:[NSNull null] forKey:hostmask]; } return matchedEntry; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCAddressBookUserTracking.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClient.h" #import "IRCAddressBookUserTracking.h" NS_ASSUME_NONNULL_BEGIN NSString * const IRCAddressBookUserTrackingStatusChangedNotification = @"IRCAddressBookUserTrackingStatusChangedNotification"; NSString * const IRCAddressBookUserTrackingAddedTrackedUserNotification = @"IRCAddressBookUserTrackingAddedTrackedUserNotification"; NSString * const IRCAddressBookUserTrackingRemovedTrackedUserNotification = @"IRCAddressBookUserTrackingRemovedTrackedUserNotification"; NSString * const IRCAddressBookUserTrackingRemovedAllTrackedUsersNotification = @"IRCAddressBookUserTrackingRemovedAllTrackedUsersNotification"; @interface IRCAddressBookUserTrackingContainer () @property (nonatomic, weak) IRCClient *client; @property (nonatomic, strong) NSMutableDictionary *trackedUsersInt; @end @implementation IRCAddressBookUserTrackingContainer - (instancetype)initWithClient:(IRCClient *)client { NSParameterAssert(client != nil); if ((self = [super init])) { self.client = client; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.trackedUsersInt = [NSMutableDictionary dictionary]; } - (void)addTrackedUser:(NSString *)nickname { NSParameterAssert(nickname != nil); @synchronized (self.trackedUsersInt) { NSString *trackingNickname = [self.trackedUsersInt keyIgnoringCase:nickname]; if (trackingNickname != nil) { return; } [self _addTrackedUser:nickname]; } } - (void)_addTrackedUser:(NSString *)nickname { NSParameterAssert(nickname != nil); @synchronized (self.trackedUsersInt) { self.trackedUsersInt[nickname] = @(NO); [RZNotificationCenter() postNotificationName:IRCAddressBookUserTrackingAddedTrackedUserNotification object:self userInfo:@{@"nickname" : nickname}]; } } - (void)removeTrackedUser:(NSString *)nickname { NSParameterAssert(nickname != nil); @synchronized (self.trackedUsersInt) { NSString *trackingNickname = [self.trackedUsersInt keyIgnoringCase:nickname]; if (trackingNickname == nil) { return; } [self _removeTrackedUser:trackingNickname]; } } - (void)_removeTrackedUser:(NSString *)nickname { NSParameterAssert(nickname != nil); @synchronized (self.trackedUsersInt) { [self.trackedUsersInt removeObjectForKey:nickname]; [RZNotificationCenter() postNotificationName:IRCAddressBookUserTrackingRemovedTrackedUserNotification object:self userInfo:@{@"nickname" : nickname}]; } } - (void)clearTrackedUsers { @synchronized (self.trackedUsersInt) { [self.trackedUsersInt removeAllObjects]; [RZNotificationCenter() postNotificationName:IRCAddressBookUserTrackingRemovedAllTrackedUsersNotification object:self]; } } - (IRCAddressBookUserTrackingStatus)statusOfUser:(NSString *)nickname { NSParameterAssert(nickname != nil); @synchronized (self.trackedUsersInt) { NSString *trackingNickname = [self.trackedUsersInt keyIgnoringCase:nickname]; if (trackingNickname == nil) { return IRCAddressBookUserTrackingStatusUnknown; } return [self _statusOfUser:nickname]; } } - (IRCAddressBookUserTrackingStatus)_statusOfUser:(NSString *)nickname { NSParameterAssert(nickname != nil); @synchronized (self.trackedUsersInt) { BOOL ison = self.trackedUsersInt[nickname].boolValue; if (ison) { return IRCAddressBookUserTrackingStatusAvailable; } else { return IRCAddressBookUserTrackingStatusNotAvailable; } } } - (IRCAddressBookUserTrackingStatus)statusOfEntry:(IRCAddressBookEntry *)addressBookEntry { NSParameterAssert(addressBookEntry != nil); NSString *trackingNickname = addressBookEntry.trackingNickname; if (trackingNickname == nil) { return IRCAddressBookUserTrackingStatusUnknown; } return [self statusOfUser:trackingNickname]; } - (NSDictionary *)trackedUsers { @synchronized (self.trackedUsersInt) { return [self.trackedUsersInt copy]; } } - (void)statusOfTrackedNickname:(NSString *)nickname changedTo:(IRCAddressBookUserTrackingStatus)newStatus { NSParameterAssert(nickname != nil); if (newStatus == IRCAddressBookUserTrackingStatusUnknown) { return; } @synchronized (self.trackedUsersInt) { NSString *trackingNickname = [self.trackedUsersInt keyIgnoringCase:nickname]; if (newStatus == IRCAddressBookUserTrackingStatusAvailable || newStatus == IRCAddressBookUserTrackingStatusSignedOn) { if (trackingNickname == nil) { trackingNickname = nickname; } self.trackedUsersInt[trackingNickname] = @(YES); } else if (newStatus == IRCAddressBookUserTrackingStatusNotAvailable || newStatus == IRCAddressBookUserTrackingStatusSignedOff) { if (trackingNickname == nil) { return; } self.trackedUsersInt[trackingNickname] = @(NO); } else if (newStatus == IRCAddressBookUserTrackingStatusNotAway || newStatus == IRCAddressBookUserTrackingStatusAway) { if (trackingNickname == nil) { return; } } [RZNotificationCenter() postNotificationName:IRCAddressBookUserTrackingStatusChangedNotification object:self userInfo:@{@"nickname" : nickname, @"status" : @(newStatus)}]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCChannel.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #include #import "NSObjectHelperPrivate.h" #import "TXMasterController.h" #import "TXWindowControllerPrivate.h" #import "TVCMainWindowPrivate.h" #import "TVCMemberListAppearance.h" #import "TVCMemberListPrivate.h" #import "TVCMemberListCellPrivate.h" #import "TVCLogControllerPrivate.h" #import "TDCSharedProtocolDefinitionsPrivate.h" #import "TDCSheetBase.h" #import "TPCPreferencesLocal.h" #import "TLOEncryptionManagerPrivate.h" #import "TLOFileLoggerPrivate.h" #import "TLOInputHistoryPrivate.h" #import "TLOLocalization.h" #import "IRCClientPrivate.h" #import "IRCChannelConfigPrivate.h" #import "IRCChannelModePrivate.h" #import "IRCChannelMemberListPrivate.h" #import "IRCChannelUserPrivate.h" #import "IRCISupportInfo.h" #import "IRCTreeItemPrivate.h" #import "IRCUser.h" #import "IRCUserRelationsPrivate.h" #import "IRCWorldPrivate.h" #import "IRCChannelPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const IRCChannelConfigurationWasUpdatedNotification = @"IRCChannelConfigurationWasUpdatedNotification"; @interface IRCChannel () @property (readonly) BOOL isSelectedChannel; @property (nonatomic, assign) BOOL statusChangedByAction; @property (nonatomic, copy, readwrite) IRCChannelConfig *config; @property (nonatomic, assign, readwrite) NSTimeInterval channelJoinTime; @property (nonatomic, strong, readwrite, nullable) IRCChannelMode *modeInfo; @property (nonatomic, strong, readwrite, nullable) IRCChannelMemberList *memberInfo; @property (nonatomic, strong, nullable) TLOFileLogger *logFile; @property (nonatomic, assign, readwrite) NSUInteger logFileSessionCount; @end @implementation IRCChannel @synthesize associatedClient = _associatedClient; - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithConfigDictionary:(NSDictionary *)dic { NSParameterAssert(dic != nil); IRCChannelConfig *config = [[IRCChannelConfig alloc] initWithDictionary:dic]; return [self initWithConfig:config]; } - (instancetype)initWithConfig:(IRCChannelConfig *)config { if ((self = [super init])) { self.config = config; [self.config writeSecretKeyToKeychain]; } return self; } - (void)updateConfig:(IRCChannelConfig *)config { [self updateConfig:config fireChangedNotification:YES updateStoredChannelList:YES]; } - (void)updateConfig:(IRCChannelConfig *)config fireChangedNotification:(BOOL)fireChangedNotification { [self updateConfig:config fireChangedNotification:fireChangedNotification updateStoredChannelList:YES]; } - (void)updateConfig:(IRCChannelConfig *)config fireChangedNotification:(BOOL)fireChangedNotification updateStoredChannelList:(BOOL)updateStoredChannelList { NSParameterAssert(config != nil); IRCChannelConfig *currentConfig = self.config; if ([config isEqual:currentConfig]) { return; } if (currentConfig.type != config.type || [currentConfig.channelName isEqualToString:config.channelName] == NO || [currentConfig.uniqueIdentifier isEqualToString:config.uniqueIdentifier] == NO) { LogToConsoleError("Tried to load configuration for incorrect channel"); return; } self.config = config; [self.config writeSecretKeyToKeychain]; if (updateStoredChannelList) { [self.associatedClient updateStoredChannelList]; } if (fireChangedNotification) { [RZNotificationCenter() postNotificationName:IRCChannelConfigurationWasUpdatedNotification object:self]; } } - (NSDictionary *)configurationDictionary { return [self.config dictionaryValue]; } - (id)copyWithZone:(nullable NSZone *)zone { /* Implement this method to allow channel to be used as a dictionary key. */ return self; } - (NSString *)description { return [NSString stringWithFormat:@"", self.associatedClient.description, self.name]; } #pragma mark - #pragma mark Property Getter - (NSString *)uniqueIdentifier { return self.config.uniqueIdentifier; } - (NSString *)name { return self.config.channelName; } - (nullable NSString *)secretKey { return self.config.secretKey; } - (BOOL)autoJoin { return self.config.autoJoin; } - (BOOL)isChannel { return (self.config.type == IRCChannelTypeChannel); } - (BOOL)isPrivateMessage { return (self.config.type == IRCChannelTypePrivateMessage); } - (BOOL)isUtility { return (self.config.type == IRCChannelTypeUtility); } - (BOOL)isPrivateMessageForZNCUser { if (self.isPrivateMessage == NO) { return NO; } IRCClient *client = self.associatedClient; return [client nicknameIsZNCUser:self.name]; } - (IRCChannelType)type { return self.config.type; } - (NSString *)channelTypeString { switch (self.config.type) { case IRCChannelTypeChannel: { return @"channel"; } case IRCChannelTypePrivateMessage: { return @"query"; } case IRCChannelTypeUtility: { return @"utility"; } } } - (nullable NSURL *)logFilePath { NSString *writePath = [TLOFileLogger writePathForItem:self]; if (writePath == nil) { return nil; } return [NSURL fileURLWithPath:writePath]; } - (nullable TVCLogLine *)lastLine { return self.viewController.lastLine; } #pragma mark - #pragma mark Property Setter - (void)setAutoJoin:(BOOL)autoJoin { if (self.isChannel == NO) { return; } if (self.autoJoin == autoJoin) { return; } IRCChannelConfigMutable *mutableConfig = [self.config mutableCopy]; mutableConfig.autoJoin = autoJoin; self.config = mutableConfig; } - (void)setName:(NSString *)name { NSParameterAssert(name != nil); if (self.isChannel) { return; } if ([self.name isEqualToString:name]) { return; } IRCChannelConfigMutable *mutableConfig = [self.config mutableCopy]; mutableConfig.channelName = name; self.config = mutableConfig; } - (void)setTopic:(nullable NSString *)topic { if (self->_topic != topic) { self->_topic = [topic copy]; [self.viewController setTopic:self->_topic]; } } #pragma mark - #pragma mark Utilities - (void)preferencesChanged { if ([TPCPreferences displayPublicMessageCountOnDockBadge] == NO) { if (self.isChannel) { self.dockUnreadCount = 0; } } } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (OTRKitMessageState)encryptionState { if ([TPCPreferences textEncryptionIsEnabled] == NO) { return OTRKitMessageStatePlaintext; } if (self.isPrivateMessage == NO) { return OTRKitMessageStatePlaintext; } IRCClient *client = self.associatedClient; return [sharedEncryptionManager() messageStateFor:[client encryptionAccountNameForUser:self.name] from:[client encryptionAccountNameForLocalUser]]; } - (BOOL)encryptionStateIsEncrypted { return ([self encryptionState] == OTRKitMessageStateEncrypted); } - (void)noteEncryptionStateDidChange { self.viewController.encrypted = self.encryptionStateIsEncrypted; [mainWindow() updateTitleFor:self]; } - (void)closeOpenEncryptionSessions { if (self.encryptionStateIsEncrypted == NO) { return; } IRCClient *client = self.associatedClient; [sharedEncryptionManager() endConversationWith:[client encryptionAccountNameForUser:self.name] from:[client encryptionAccountNameForLocalUser]]; } #endif #pragma mark - #pragma mark Channel Status - (void)setStatus:(IRCChannelStatus)status { if (self->_status != status) { self->_status = status; [self performActionOnStatusChange]; } } - (void)performActionOnStatusChange { if (self.statusChangedByAction) { self.statusChangedByAction = NO; return; } if (self.status == IRCChannelStatusJoined) { [self activate]; } else if (self.status == IRCChannelStatusParted) { [self deactivate]; } } - (void)resetStatus:(IRCChannelStatus)toStatus { if (toStatus == IRCChannelStatusJoining) { return; } self.channelModesReceived = NO; self.channelNamesReceived = NO; self.errorOnLastJoinAttempt = NO; self.sentInitialWhoRequest = NO; self.channelJoinTime = 0; self.modeInfo = nil; self.status = toStatus; self.topic = nil; /* Clearing members, instead of just declaring memberInfo nil, is important so that all users can be properly disassociated with this channel. There are many relations. */ [self clearMembers]; self.memberInfo = nil; } - (void)activate { self.statusChangedByAction = YES; [self resetStatus:IRCChannelStatusJoined]; IRCClient *client = self.associatedClient; if (self.isUtility == NO) { self.memberInfo = [[IRCChannelMemberList alloc] initWithChannel:self]; /* A channel is only assigned by TVCMainWindow when it's switched to. Because of that we have to assign it here as the user already has the channel selected, but it was never assigned because the member list did not exist up to this point. */ if (self.isSelectedChannel) { [mainWindowMemberList() assignToChannel:self]; } } if (self.isChannel) { [client postEventToViewController:@"channelJoined" forChannel:self]; self.modeInfo = [[IRCChannelMode alloc] initWithChannel:self]; } if (self.isPrivateMessage) { IRCUser *user1 = [self.associatedClient findUserOrCreate:self.name]; [self addUser:user1]; IRCUser *user2 = [self.associatedClient findUserOrCreate:client.userNickname]; [self addUser:user2]; } self.channelJoinTime = [NSDate timeIntervalSince1970]; } - (void)deactivate { #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 if (self.isPrivateMessage) { [self closeOpenEncryptionSessions]; } #endif self.statusChangedByAction = YES; [self resetStatus:IRCChannelStatusParted]; if (self.isChannel) { [self.associatedClient postEventToViewController:@"channelParted" forChannel:self]; } } - (void)prepareForPermanentDestruction { self.statusChangedByAction = YES; [self resetStatus:IRCChannelStatusTerminated]; [self closeLogFile]; [self.config destroySecretKeyKeychainItem]; NSArray *openWindows = [windowController() windowsFromWindowList:@[@"TDCChannelPropertiesSheet", @"TDCChannelModifyTopicSheet", @"TDCChannelModifyModesSheet", @"TDCChannelBanListSheet"]]; for (TDCSheetBase *windowObject in openWindows) { if ([windowObject.channelId isEqualToString:self.uniqueIdentifier]) { [windowObject close]; } } [[mainWindow() inputHistoryManager] destroy:self]; [self.viewController prepareForPermanentDestruction]; } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Preparing channel: <%{public}@>", self.uniqueIdentifier); self.statusChangedByAction = YES; LogToConsoleTerminationProgress("[%{public}@] Resetting status to terminated", self.uniqueIdentifier); [self resetStatus:IRCChannelStatusTerminated]; LogToConsoleTerminationProgress("[%{public}@] Closing log file", self.uniqueIdentifier); [self closeLogFile]; if (self.isPrivateMessage) { LogToConsoleTerminationProgress("[%{public}@] Destroying keychain items for private message", self.uniqueIdentifier); [self.config destroySecretKeyKeychainItem]; } LogToConsoleTerminationProgress("[%{public}@] Preparing view controller: <%{public}@>", self.uniqueIdentifier, self.viewController.uniqueIdentifier); [self.viewController prepareForApplicationTermination]; } #pragma mark - #pragma mark Log File - (void)reopenLogFileIfNeeded { if ([TPCPreferences logToDiskIsEnabled] && self.isUtility == NO) { if ( self.logFile) { [self.logFile reopenIfNeeded]; } } else { [self closeLogFile]; } } - (void)closeLogFile { if (self.logFile == nil) { return; } [self.logFile close]; } - (void)logFileWriteSessionBegin { [self.associatedClient logFileRecordSessionChanged:YES inChannel:self]; } - (void)logFileWriteSessionEnd { [self.associatedClient logFileRecordSessionChanged:NO inChannel:self]; self.logFileSessionCount = 0; } #pragma mark - #pragma mark Printing - (void)writeToLogLineToLogFile:(TVCLogLine *)logLine { NSParameterAssert(logLine != nil); if (self.isUtility || [TPCPreferences logToDiskIsEnabled] == NO) { return; } // Perform addition before if statement to avoid infinite loop self.logFileSessionCount += 1; if (self.logFileSessionCount == 1) { [self logFileWriteSessionBegin]; } if (self.logFile == nil) { self.logFile = [[TLOFileLogger alloc] initWithChannel:self]; } [self.logFile writeLogLine:logLine]; } - (void)print:(TVCLogLine *)logLine { [self print:logLine completionBlock:nil]; } - (void)print:(TVCLogLine *)logLine completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock { NSParameterAssert(logLine != nil); [self.viewController print:logLine completionBlock:completionBlock]; [self writeToLogLineToLogFile:logLine]; } #pragma mark - #pragma mark Member List - (void)addUser:(IRCUser *)user { [self.memberInfo addUser:user]; } - (void)addMember:(IRCChannelUser *)member { [self.memberInfo addMember:member]; } - (void)addMember:(IRCChannelUser *)member checkForDuplicates:(BOOL)checkForDuplicates { [self.memberInfo addMember:member checkForDuplicates:checkForDuplicates]; } - (void)removeMemberWithNickname:(NSString *)nickname { [self.memberInfo removeMemberWithNickname:nickname]; } - (void)removeMember:(IRCChannelUser *)member { [self.memberInfo removeMember:member]; } - (void)resortMember:(IRCChannelUser *)member { [self.memberInfo resortMember:member]; } - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 { [self.memberInfo replaceMember:member1 withMember:member2]; } - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 resort:(BOOL)resort { [self.memberInfo replaceMember:member1 withMember:member2 resort:resort]; } - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 resort:(BOOL)resort replaceInAllChannels:(BOOL)replaceInAllChannels { [self.memberInfo replaceMember:member1 withMember:member2 resort:resort replaceInAllChannels:replaceInAllChannels]; } - (void)changeMember:(NSString *)nickname mode:(NSString *)mode value:(BOOL)value { [self.memberInfo changeMember:nickname mode:mode value:value]; } - (void)clearMembers { [self.memberInfo clearMembers]; } - (NSUInteger)numberOfMembers { return self.memberInfo.numberOfMembers; } - (nullable NSArray *)memberList { return self.memberInfo.memberList; } - (NSData *)pasteboardDataForMembers:(NSArray *)members { return [self.memberInfo pasteboardDataForMembers:members]; } + (BOOL)readNicknamesFromPasteboardData:(NSData *)pasteboardData withBlock:(void (NS_NOESCAPE ^)(IRCChannel *channel, NSArray *nicknames))callbackBlock { return [IRCChannelMemberList readNicknamesFromPasteboardData:pasteboardData withBlock:callbackBlock]; } + (BOOL)readMembersFromPasteboardData:(NSData *)pasteboardData withBlock:(void (NS_NOESCAPE ^)(IRCChannel *channel, NSArray *members))callbackBlock { return [IRCChannelMemberList readMembersFromPasteboardData:pasteboardData withBlock:callbackBlock]; } - (BOOL)memberExists:(NSString *)nickname { return [self.memberInfo memberExists:nickname]; } - (nullable IRCChannelUser *)findMember:(NSString *)nickname { return [self.memberInfo findMember:nickname]; } - (void)sortMembers { [self.memberInfo sortMembers]; } #pragma mark - #pragma mark Table View Delegate - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { NSView *newView = [tableView makeViewWithIdentifier:@"GroupView" owner:self]; return newView; } - (nullable NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row { TVCMemberListRowCell *rowView = [[TVCMemberListRowCell alloc] initWithMemberList:(id)tableView]; return rowView; } - (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row { [mainWindowMemberList() refreshDrawingForRow:row]; } #pragma mark - #pragma mark IRCTreeItem Properties - (BOOL)isSelectedChannel { return (self == mainWindow().selectedItem); } - (BOOL)isActive { return (self.status == IRCChannelStatusJoined); } - (BOOL)isClient { return NO; } - (NSUInteger)numberOfChildren { return 0; } - (nullable id)childAtIndex:(NSUInteger)index { return nil; } - (NSString *)label { NSString *customLabel = self.config.label; if (customLabel.length > 0) { return customLabel; } return self.name; } - (nullable IRCClient *)associatedClient { return self->_associatedClient; } - (nullable IRCChannel *)associatedChannel { return self; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCChannelConfig.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TPCPreferencesLocalPrivate.h" #import "IRCChannelConfigInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCChannelConfig #pragma mark - #pragma mark Defaults - (void)populateDefaultsPreflight { if (self.initializedAsCopy) { return; } self->_defaults = @{ @"autoJoin" : @(YES), @"channelType" : @(IRCChannelTypeChannel), @"ignoreGeneralEventMessages" : @(NO), @"ignoreHighlights" : @(NO), @"inlineMediaEnabled" : @(NO), @"inlineMediaDisabled" : @(NO), @"pushNotifications" : @(YES), @"showTreeBadgeCount" : @(YES) }; } - (void)populateDefaultsPostflight { if (self.initializedAsCopy) { return; } SetVariableIfNil(self->_channelName, @"") SetVariableIfNil(self->_uniqueIdentifier, [NSString stringWithUUID]) SetVariableIfNil(self->_notificationsMutable, [NSMutableDictionary dictionary]) } - (void)populateDefaultsByAppendingDictionary:(NSDictionary *)defaultsToAppend { NSParameterAssert(defaultsToAppend != nil); self->_defaults = [self->_defaults dictionaryByAddingEntries:defaultsToAppend]; } #pragma mark - #pragma mark Channel Configuration + (IRCChannelConfig *)seedWithName:(NSString *)channelName { NSParameterAssert(channelName != nil); NSDictionary *dic = @{@"channelName" : channelName}; IRCChannelConfig *config = [[self alloc] initWithDictionary:dic]; return config; } - (instancetype)init { return [super initWithDictionary:@{}]; } - (void)initializedClassHealthCheck { if (self.mutable || self.initializedAsCopy) { return; } NSParameterAssert(self->_channelName.length > 0); } - (void)populateDictionaryValues:(NSDictionary *)dic { NSParameterAssert(dic != nil); NSMutableDictionary *defaultsMutable = [self->_defaults mutableCopy]; [defaultsMutable addEntriesFromDictionary:dic]; [defaultsMutable assignBoolTo:&self->_pushNotifications forKey:@"pushNotifications"]; [defaultsMutable assignBoolTo:&self->_showTreeBadgeCount forKey:@"showTreeBadgeCount"]; [defaultsMutable assignStringTo:&self->_channelName forKey:@"channelName"]; [defaultsMutable assignStringTo:&self->_uniqueIdentifier forKey:@"uniqueIdentifier"]; [defaultsMutable assignUnsignedIntegerTo:&self->_type forKey:@"channelType"]; if (self->_type != IRCChannelTypeChannel) { return; } /* Load the newest set of keys */ [defaultsMutable assignBoolTo:&self->_autoJoin forKey:@"autoJoin"]; [defaultsMutable assignBoolTo:&self->_ignoreGeneralEventMessages forKey:@"ignoreGeneralEventMessages"]; [defaultsMutable assignBoolTo:&self->_ignoreHighlights forKey:@"ignoreHighlights"]; [defaultsMutable assignBoolTo:&self->_inlineMediaDisabled forKey:@"inlineMediaDisabled"]; [defaultsMutable assignBoolTo:&self->_inlineMediaEnabled forKey:@"inlineMediaEnabled"]; [defaultsMutable assignStringTo:&self->_label forKey:@"label"]; [defaultsMutable assignStringTo:&self->_defaultModes forKey:@"defaultMode"]; [defaultsMutable assignStringTo:&self->_defaultTopic forKey:@"defaultTopic"]; NSDictionary *notifications = [defaultsMutable dictionaryForKey:@"notifications"]; if (notifications != nil) { self->_notificationsMutable = [notifications mutableCopy]; } /* Load legacy keys (if they exist) */ if (self.initializedAsCopy) { return; } [defaultsMutable assignBoolTo:&self->_autoJoin forKey:@"joinOnConnect"]; [defaultsMutable assignBoolTo:&self->_ignoreGeneralEventMessages forKey:@"ignoreJPQActivity"]; [defaultsMutable assignBoolTo:&self->_pushNotifications forKey:@"enableNotifications"]; [defaultsMutable assignBoolTo:&self->_showTreeBadgeCount forKey:@"enableTreeBadgeCountDrawing"]; /* Migrate inline media */ /* Old behavior was to store a single property named "ignoreInlineMedia" Depending on the value of the global preference, the value of this property was used to determine whether to hide inline media per-channel or to show it per-channel. That is stupid idea because if someone has it enabled globally, has it turned off in a channel, turns it off globally, then it is turned on in that channel. We split it up into two properties and this logic performs migration. */ { /* Do new keys exist in incoming dictionary/ */ if (dic[@"inlineMediaEnabled"] != nil && dic[@"inlineMediaDisabled"] != nil) { return; } NSNumber *ignoreInlineMedia = dic[@"ignoreInlineMedia"]; // old key /* If old value is NO, then we do not have to continue because the default value for the new values is NO. */ if (ignoreInlineMedia == nil || ignoreInlineMedia.boolValue == NO) { return; } BOOL inlineEnabledGlobally = [TPCPreferences showInlineMedia]; /* Old property was the inverse of the global */ /* Global enabled = local disabled, Global disabled = local enabled */ self->_inlineMediaDisabled = inlineEnabledGlobally; self->_inlineMediaEnabled = !inlineEnabledGlobally; } } - (NSDictionary *)dictionaryValueForTarget:(XRPortablePropertyDictTarget)target { NSMutableDictionary *dic = [NSMutableDictionary dictionary]; [dic setBool:self.pushNotifications forKey:@"pushNotifications"]; [dic setBool:self.showTreeBadgeCount forKey:@"showTreeBadgeCount"]; if (self.type == IRCChannelTypeChannel) { [dic maybeSetObject:self.label forKey:@"label"]; [dic maybeSetObject:self.defaultModes forKey:@"defaultMode"]; [dic maybeSetObject:self.defaultTopic forKey:@"defaultTopic"]; [dic maybeSetObject:self.notifications forKey:@"notifications"]; [dic setBool:self.autoJoin forKey:@"autoJoin"]; [dic setBool:self.ignoreGeneralEventMessages forKey:@"ignoreGeneralEventMessages"]; [dic setBool:self.ignoreHighlights forKey:@"ignoreHighlights"]; [dic setBool:self.inlineMediaDisabled forKey:@"inlineMediaDisabled"]; [dic setBool:self.inlineMediaEnabled forKey:@"inlineMediaEnabled"]; } [dic maybeSetObject:self.channelName forKey:@"channelName"]; [dic maybeSetObject:self.uniqueIdentifier forKey:@"uniqueIdentifier"]; [dic setUnsignedInteger:self.type forKey:@"channelType"]; if (target == XRPortablePropertyDictTargetCopy || target == XRPortablePropertyDictTargetMutableCopy) { return [dic copy]; } return [dic dictionaryByRemovingDefaults:self->_defaults allowEmptyValues:YES]; } - (BOOL)isEqual:(id)object { if (object == nil) { return NO; } if (object == self) { return YES; } if ([object isKindOfClass:[IRCChannelConfig class]] == NO) { return NO; } IRCChannelConfig *objectCast = (IRCChannelConfig *)object; NSDictionary *s1 = self.dictionaryValue; NSDictionary *s2 = objectCast.dictionaryValue; return ([s1 isEqualToDictionary:s2] && ((self->_secretKey == nil && objectCast->_secretKey == nil) || [self->_secretKey isEqualToString:objectCast->_secretKey])); } - (NSUInteger)hash { return self.uniqueIdentifier.hash; } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { IRCChannelConfig *config = [self allocForCopyAsMutable:mutableCopy]; config->_defaults = self->_defaults; config->_secretKey = self->_secretKey; if (uniquing) { config->_uniqueIdentifier = [NSString stringWithUUID]; } return [config initWithDictionary:self.dictionaryValueForCopy]; } - (__kindof XRPortablePropertyDict *)mutableClass { return [IRCChannelConfigMutable self]; } - (NSDictionary *)notifications { @synchronized (self->_notificationsMutable) { return [self->_notificationsMutable copy]; } } #pragma mark - #pragma mark Keychain Management - (nullable NSString *)secretKey { if (self->_secretKey) { return self->_secretKey; } return self.secretKeyFromKeychain; } - (nullable NSString *)secretKeyFromKeychain { NSString *secretKeyServiceName = [NSString stringWithFormat:@"textual.cjoinkey.%@", self.uniqueIdentifier]; NSString *kcPassword = [XRKeychain getPasswordFromKeychainItem:@"Textual (Channel JOIN Key)" withItemKind:@"application password" forUsername:nil serviceName:secretKeyServiceName]; return kcPassword; } - (void)writeSecretKeyToKeychain { if (self->_secretKey == nil) { return; } NSString *secretKeyServiceName = [NSString stringWithFormat:@"textual.cjoinkey.%@", self.uniqueIdentifier]; [XRKeychain modifyOrAddKeychainItem:@"Textual (Channel JOIN Key)" withItemKind:@"application password" forUsername:nil withNewPassword:self->_secretKey serviceName:secretKeyServiceName]; self->_secretKey = nil; } - (void)destroySecretKeyKeychainItem { NSString *secretKeyServiceName = [NSString stringWithFormat:@"textual.cjoinkey.%@", self.uniqueIdentifier]; [XRKeychain deleteKeychainItem:@"Textual (Channel JOIN Key)" withItemKind:@"application password" forUsername:nil serviceName:secretKeyServiceName]; /* Reset temporary value */ self->_secretKey = nil; } #pragma mark - #pragma mark Notifications - (nullable NSString *)soundForEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Sound"]; if (eventKey == nil) { return nil; } /* @synchronized is used here because IRCChannelConfigMutable can modify this value underneath us. */ @synchronized (self->_notificationsMutable) { return self->_notificationsMutable[eventKey]; } } - (NSControlStateValue)_stateForEventKey:(NSString *)eventKey { NSParameterAssert(eventKey != nil); @synchronized (self->_notificationsMutable) { NSNumber *value = self->_notificationsMutable[eventKey]; if (value == nil) { return NSControlStateValueMixed; } if (value.boolValue == NO) { return NSControlStateValueOff; } return NSControlStateValueOn; } } - (NSControlStateValue)notificationEnabledForEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Enabled"]; if (eventKey == nil) { return NO; } return [self _stateForEventKey:eventKey]; } - (NSControlStateValue)disabledWhileAwayForEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Disable While Away"]; if (eventKey == nil) { return NO; } return [self _stateForEventKey:eventKey]; } - (NSControlStateValue)bounceDockIconForEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Bounce Dock Icon"]; if (eventKey == nil) { return NO; } return [self _stateForEventKey:eventKey]; } - (NSControlStateValue)bounceDockIconRepeatedlyForEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Bounce Dock Icon Repeatedly"]; if (eventKey == nil) { return NO; } return [self _stateForEventKey:eventKey]; } - (NSControlStateValue)speakEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Speak"]; if (eventKey == nil) { return NO; } return [self _stateForEventKey:eventKey]; } @end #pragma mark - @implementation IRCChannelConfigMutable @dynamic type; @dynamic autoJoin; @dynamic channelName; @dynamic label; @dynamic defaultModes; @dynamic defaultTopic; @dynamic ignoreGeneralEventMessages; @dynamic ignoreHighlights; @dynamic inlineMediaDisabled; @dynamic inlineMediaEnabled; @dynamic pushNotifications; @dynamic secretKey; @dynamic showTreeBadgeCount; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyDict *)immutableClass { return [IRCChannelConfig self]; } - (void)setType:(IRCChannelType)type { if (self->_type != type) { self->_type = type; } } - (void)setAutoJoin:(BOOL)autoJoin { if (self->_autoJoin != autoJoin) { self->_autoJoin = autoJoin; } } - (void)setIgnoreGeneralEventMessages:(BOOL)ignoreGeneralEventMessages { if (self->_ignoreGeneralEventMessages != ignoreGeneralEventMessages) { self->_ignoreGeneralEventMessages = ignoreGeneralEventMessages; } } - (void)setIgnoreHighlights:(BOOL)ignoreHighlights { if (self->_ignoreHighlights != ignoreHighlights) { self->_ignoreHighlights = ignoreHighlights; } } - (void)setInlineMediaDisabled:(BOOL)inlineMediaDisabled { if (self->_inlineMediaDisabled != inlineMediaDisabled) { self->_inlineMediaDisabled = inlineMediaDisabled; } } - (void)setInlineMediaEnabled:(BOOL)inlineMediaEnabled { if (self->_inlineMediaEnabled != inlineMediaEnabled) { self->_inlineMediaEnabled = inlineMediaEnabled; } } - (void)setPushNotifications:(BOOL)pushNotifications { if (self->_pushNotifications != pushNotifications) { self->_pushNotifications = pushNotifications; } } - (void)setShowTreeBadgeCount:(BOOL)showTreeBadgeCount { if (self->_showTreeBadgeCount != showTreeBadgeCount) { self->_showTreeBadgeCount = showTreeBadgeCount; } } - (void)setChannelName:(NSString *)channelName { NSParameterAssert(channelName != nil); if (self->_channelName != channelName) { self->_channelName = [channelName copy]; } } - (void)setLabel:(nullable NSString *)label { if (self->_label != label) { self->_label = [label copy]; } } - (void)setDefaultModes:(nullable NSString *)defaultModes { if (self->_defaultModes != defaultModes) { self->_defaultModes = [defaultModes copy]; } } - (void)setDefaultTopic:(nullable NSString *)defaultTopic { if (self->_defaultTopic != defaultTopic) { self->_defaultTopic = [defaultTopic copy]; } } - (void)setSecretKey:(nullable NSString *)secretKey { if (self->_secretKey != secretKey) { self->_secretKey = [secretKey copy]; } } - (void)setSound:(nullable NSString *)value forEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Sound"]; if (eventKey == nil) { return; } @synchronized (self->_notificationsMutable) { if (value == nil) { [self->_notificationsMutable removeObjectForKey:eventKey]; } else { self->_notificationsMutable[eventKey] = value; } } } - (void)_setState:(NSControlStateValue)state forEventKey:(NSString *)eventKey { NSParameterAssert(eventKey != nil); @synchronized (self->_notificationsMutable) { switch (state) { case NSControlStateValueOn: { self->_notificationsMutable[eventKey] = @(YES); break; } case NSControlStateValueOff: { self->_notificationsMutable[eventKey] = @(NO); break; } case NSControlStateValueMixed: { [self->_notificationsMutable removeObjectForKey:eventKey]; break; } default: { NSAssert(NO, @"Bad 'state'"); } } } } - (void)setNotificationEnabled:(NSControlStateValue)value forEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Enabled"]; if (eventKey == nil) { return; } [self _setState:value forEventKey:eventKey]; } - (void)setDisabledWhileAway:(NSControlStateValue)value forEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Disable While Away"]; if (eventKey == nil) { return; } [self _setState:value forEventKey:eventKey]; } - (void)setBounceDockIcon:(NSControlStateValue)value forEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Bounce Dock Icon"]; if (eventKey == nil) { return; } [self _setState:value forEventKey:eventKey]; } - (void)setBounceDockIconRepeatedly:(NSControlStateValue)value forEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Bounce Dock Icon Repeatedly"]; if (eventKey == nil) { return; } [self _setState:value forEventKey:eventKey]; } - (void)setEventIsSpoken:(NSControlStateValue)value forEvent:(TXNotificationType)event { NSString *eventKey = [TPCPreferences keyForEvent:event category:@"Speak"]; if (eventKey == nil) { return; } [self _setState:value forEventKey:eventKey]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCChannelMemberList.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual and/or Codeux Software, nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCClientPrivate.h" #import "IRCChannelPrivate.h" #import "IRCChannelMemberListPrivate.h" #import "IRCChannelMemberListControllerPrivate.h" #import "IRCChannelUserPrivate.h" #import "IRCISupportInfo.h" #import "IRCUserRelationsPrivate.h" #import "IRCUserPrivate.h" #import "IRCWorld.h" #import "TPCPreferencesLocal.h" #import "TVCMemberList.h" #import "TVCMainWindow.h" #import "TXMasterController.h" NS_ASSUME_NONNULL_BEGIN @interface IRCChannelMemberList () @property (nonatomic, weak) IRCClient *client; @property (nonatomic, weak) IRCChannel *channel; @property (nonatomic, strong, nullable) IRCChannelMemberListController *controller; @property (nonatomic, strong) NSMutableArray *memberContainer; @end @implementation IRCChannelMemberList - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ((self = [super init])) { self.client = channel.associatedClient; self.channel = channel; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.memberContainer = [NSMutableArray array]; } - (void)dealloc { /* Send a last message before death. */ [self unassignController]; } - (void)assignController:(nullable IRCChannelMemberListController *)controller { /* All modifications to the controller occur on the main thread. The controller is a UI object which requires updates on the main thread in addition to the safety it provides us again race conditions. */ XRPerformBlockSynchronouslyOnMainQueue(^{ [controller replaceContents:self.memberList]; self.controller = controller; }); } - (void)unassignController { XRPerformBlockSynchronouslyOnMainQueue(^{ __weak IRCChannelMemberListController *controller = self.controller; if ( controller) { [controller assignToChannel:nil]; } }); } #pragma mark - #pragma mark Grand Central Dispatch /* All modifications to the member list occur on this serial queue to guarantee that there is only ever one person accessing the mutable store at any given time. */ + (dispatch_queue_t)modifyMemberListSerialQueue { static dispatch_queue_t workerQueue = NULL; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ workerQueue = XRCreateDispatchQueueWithPriority("IRCChannel.modifyMemberListSerialQueue", DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT); }); return workerQueue; } + (void)resumeMemberListSerialQueues { dispatch_resume([self modifyMemberListSerialQueue]); } + (void)suspendMemberListSerialQueues { dispatch_suspend([self modifyMemberListSerialQueue]); } + (void)accessMemberListUsingBlock:(dispatch_block_t)block { NSCParameterAssert(block != NULL); dispatch_queue_t workerQueue = [self modifyMemberListSerialQueue]; static void *IsOnWorkerQueueKey = NULL; if (IsOnWorkerQueueKey == NULL) { IsOnWorkerQueueKey = &IsOnWorkerQueueKey; dispatch_queue_set_specific(workerQueue, IsOnWorkerQueueKey, (void *)1, NULL); } if (dispatch_get_specific(IsOnWorkerQueueKey)) { block(); return; } dispatch_sync(workerQueue, ^{ @autoreleasepool { block(); } }); } #pragma mark - #pragma mark Backend Operations - (NSUInteger)nonatomic_sortedIndexForMember:(IRCChannelUser *)member { NSParameterAssert(member != nil); NSMutableArray *container = self.memberContainer; NSUInteger index = [container indexOfObject:member inSortedRange:container.range options:NSBinarySearchingInsertionIndex usingComparator:[IRCChannelUser channelRankComparator]]; return index; } - (NSInteger)nonatomic_sortedInsert:(IRCChannelUser *)member { NSParameterAssert(member != nil); NSInteger insertedIndex = [self nonatomic_sortedIndexForMember:member]; [self.memberContainer insertObject:member atIndex:insertedIndex]; return insertedIndex; } - (NSInteger)nonatomic_replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 { NSParameterAssert(member1 != nil); NSParameterAssert(member2 != nil); NSMutableArray *container = self.memberContainer; NSUInteger index = [container indexOfObjectIdenticalTo:member1]; if (index == NSNotFound) { return (-1); } container[index] = member2; return index; } - (NSInteger)nonatomic_removeMember:(IRCChannelUser *)member { NSParameterAssert(member != nil); NSMutableArray *container = self.memberContainer; NSUInteger index = [container indexOfObjectIdenticalTo:member]; if (index == NSNotFound) { return (-1); } [container removeObjectAtIndex:index]; return index; } #pragma mark - #pragma mark Frontend Operations - (void)addUser:(IRCUser *)user { NSParameterAssert(user != nil); IRCChannelUser *member = [[IRCChannelUser alloc] initWithUser:user]; [self addMember:member]; } - (void)addMember:(IRCChannelUser *)member { [self addMember:member checkForDuplicates:NO]; } - (void)addMember:(IRCChannelUser *)member checkForDuplicates:(BOOL)checkForDuplicates { NSParameterAssert(member != nil); IRCChannel *channel = self.channel; if (checkForDuplicates) { IRCChannelUser *oldMember = [member.user userAssociatedWithChannel:channel]; if (oldMember != nil) { [self replaceMember:oldMember withMember:member]; return; } } if ([member isKindOfClass:[IRCChannelUserMutable class]]) { member = [member copy]; } [member associateWithChannel:channel]; [self willChangeValueForKey:@"numberOfMembers"]; [self willChangeValueForKey:@"memberList"]; __block NSInteger sortedIndex = (-1); [self.class accessMemberListUsingBlock:^{ sortedIndex = [self nonatomic_sortedInsert:member]; }]; [self didChangeValueForKey:@"numberOfMembers"]; [self didChangeValueForKey:@"memberList"]; if (channel.isChannel == NO) { return; } XRPerformBlockSynchronouslyOnMainQueue(^{ __weak IRCChannelMemberListController *controller = self.controller; if ( controller != nil) { [controller insertObject:member atArrangedObjectIndex:sortedIndex]; } [self.client postEventToViewController:@"channelMemberAdded" forChannel:channel]; }); } - (void)removeMemberWithNickname:(NSString *)nickname { NSParameterAssert(nickname != nil); IRCChannelUser *member = [self findMember:nickname]; if (member) { [self removeMember:member]; } } - (void)removeMember:(IRCChannelUser *)member { NSParameterAssert(member != nil); IRCChannel *channel = self.channel; [member disassociateWithChannel:channel]; __block NSInteger sortedIndex = (-1); [self.class accessMemberListUsingBlock:^{ sortedIndex = [self nonatomic_removeMember:member]; }]; if (sortedIndex < 0 || channel.isChannel == NO) { return; } XRPerformBlockSynchronouslyOnMainQueue(^{ __weak IRCChannelMemberListController *controller = self.controller; if ( controller != nil) { [controller removeObjectAtArrangedObjectIndex:sortedIndex]; } [self.client postEventToViewController:@"channelMemberRemoved" forChannel:channel]; }); } - (void)resortMember:(IRCChannelUser *)member { NSParameterAssert(member != nil); if ([member isKindOfClass:[IRCChannelUserMutable class]]) { member = [member copy]; } [self replaceMember:member withMember:member resort:YES]; } - (void)_replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 resort:(BOOL)resort { NSParameterAssert(member1 != nil); NSParameterAssert(member2 != nil); IRCChannel *channel = self.channel; if (member1 != member2) { [member1 disassociateWithChannel:channel]; [member2 associateWithChannel:channel]; } __block NSInteger oldIndex = (-1); __block NSInteger newIndex = (-1); [self.class accessMemberListUsingBlock:^{ if (resort) { oldIndex = [self nonatomic_removeMember:member1]; newIndex = [self nonatomic_sortedInsert:member2]; } else { newIndex = [self nonatomic_replaceMember:member1 withMember:member2]; } }]; if (newIndex < 0 || channel.isChannel == NO) { return; } XRPerformBlockSynchronouslyOnMainQueue(^{ __weak IRCChannelMemberListController *controller = self.controller; if (controller == nil) { return; } [mainWindowMemberList() beginUpdates]; if (resort) { if (oldIndex >= 0) { [controller removeObjectAtArrangedObjectIndex:oldIndex]; } [controller insertObject:member2 atArrangedObjectIndex:newIndex]; } else { [mainWindowMemberList() refreshDrawingForRow:newIndex]; } [mainWindowMemberList() endUpdates]; }); } - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 { [self replaceMember:member1 withMember:member2 resort:YES replaceInAllChannels:NO]; } - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 resort:(BOOL)resort { [self replaceMember:member1 withMember:member2 resort:YES replaceInAllChannels:NO]; } - (void)replaceMember:(IRCChannelUser *)member1 withMember:(IRCChannelUser *)member2 resort:(BOOL)resort replaceInAllChannels:(BOOL)replaceInAllChannels { NSParameterAssert(member1 != nil); NSParameterAssert(member2 != nil); if ([member2 isKindOfClass:[IRCChannelUserMutable class]]) { member2 = [member2 copy]; } [self _replaceMember:member1 withMember:member2 resort:resort]; if (replaceInAllChannels) { IRCChannel *thisChannel = self.channel; NSDictionary *relations = member2.user.relations; [relations enumerateKeysAndObjectsUsingBlock:^(IRCChannel *targetChannel, IRCChannelUser *member, BOOL *stop) { if (thisChannel == targetChannel) { return; } IRCChannelMemberList *memberList = targetChannel.memberInfo; [memberList _replaceMember:member withMember:member resort:resort]; }]; } } - (void)changeMember:(NSString *)nickname mode:(NSString *)mode value:(BOOL)value { NSParameterAssert(nickname != nil); NSParameterAssert(mode.length == 1); IRCClient *client = self.client; IRCChannel *channel = self.channel; // Find member and create mutable copy for editing IRCChannelUser *member = [channel findMember:nickname]; if (member == nil) { return; } IRCChannelUserMutable *memberMutable = [member mutableCopy]; NSString *oldMemberModes = memberMutable.modes; // If the member has no modes already and we are setting a mode, then // all we have to do is set the value of -modes to new mode BOOL processModes = YES; if (oldMemberModes.length == 0) { if (value) { processModes = NO; memberMutable.modes = mode; } else { return; // Can't remove mode from empty string } } else { if (value && [oldMemberModes contains:mode]) { return; // Mode is already in string } } // Split up the current user modes into an array of characters. // Enumerate over the array of characters to find which mode in the // current set has a rank lower than the mode being inserted. // Insert before the lower ranked mode or insert at end. if (processModes) { IRCISupportInfo *clientSupportInfo = client.supportInfo; NSArray *oldModeSymbols = oldMemberModes.characterStringBuffer; NSMutableArray *newModeSymbols = [oldModeSymbols mutableCopy]; if (value == NO) { [newModeSymbols removeObject:mode]; } else { NSUInteger rankOfNewMode = [clientSupportInfo rankForUserPrefixWithMode:mode]; NSUInteger lowerRankedMode = [oldModeSymbols indexOfObjectPassingTest:^BOOL(NSString *oldModeSymbol, NSUInteger index, BOOL *stop) { NSInteger rankOfOldMode = [clientSupportInfo rankForUserPrefixWithMode:oldModeSymbol]; return (rankOfOldMode < rankOfNewMode); }]; if (lowerRankedMode != NSNotFound) { [newModeSymbols insertObject:mode atIndex:lowerRankedMode]; } else { [newModeSymbols addObject:mode]; } } NSString *newMemberModes = [newModeSymbols componentsJoinedByString:@""]; memberMutable.modes = newMemberModes; } BOOL replaceInAllChannels = NO; if (value && [mode isEqualToString:@"Y"] && member.user.isIRCop == NO) { /* InspIRCd treats +Y as an IRCop. */ /* If the user wasn't already marked as an IRCop, then we mark them at this point. */ [client modifyUser:member.user withBlock:^(IRCUserMutable *userMutable) { userMutable.isIRCop = YES; }]; if ([TPCPreferences memberListSortFavorsServerStaff]) { replaceInAllChannels = YES; } } // Remove the user from the member list and insert sorted [self replaceMember:member withMember:memberMutable resort:YES replaceInAllChannels:replaceInAllChannels]; } #pragma mark - #pragma mark Utilities - (void)sortMembers { [self.class accessMemberListUsingBlock:^{ [self.memberContainer sortUsingComparator:[IRCChannelUser channelRankComparator]]; }]; XRPerformBlockSynchronouslyOnMainQueue(^{ __weak IRCChannelMemberListController *controller = self.controller; if (controller == nil) { return; } [controller replaceContents:self.memberList]; }); } - (void)clearMembers { IRCChannel *channel = self.channel; [self.class accessMemberListUsingBlock:^{ [self willChangeValueForKey:@"numberOfMembers"]; [self willChangeValueForKey:@"memberList"]; [self.memberContainer makeObjectsPerformSelector:@selector(disassociateWithChannel:) withObject:channel]; [self.memberContainer removeAllObjects]; [self didChangeValueForKey:@"numberOfMembers"]; [self didChangeValueForKey:@"memberList"]; }]; XRPerformBlockSynchronouslyOnMainQueue(^{ __weak IRCChannelMemberListController *controller = self.controller; if (controller == nil) { return; } [controller replaceContents:@[]]; }); } - (NSUInteger)numberOfMembers { __block NSUInteger memberCount = 0; [self.class accessMemberListUsingBlock:^{ memberCount = self.memberContainer.count; }]; return memberCount; } - (nullable NSArray *)memberList { __block NSArray *memberList = nil; [self.class accessMemberListUsingBlock:^{ memberList = [self.memberContainer copy]; }]; return memberList; } #pragma mark - #pragma mark Clipboard - (NSData *)pasteboardDataForMembers:(NSArray *)members { NSParameterAssert(members != nil); NSString *channelId = self.channel.uniqueIdentifier; NSMutableArray *nicknames = [NSMutableArray arrayWithCapacity:members.count]; for (IRCChannelUser *member in members) { [nicknames addObject:member.user.nickname]; } NSDictionary *pasteboardDictionary = @{ @"channelId" : channelId, @"nicknames" : nicknames }; NSData *pasteboardData = [NSKeyedArchiver archivedDataWithRootObject:pasteboardDictionary]; return pasteboardData; } + (BOOL)readNicknamesFromPasteboardData:(NSData *)pasteboardData withBlock:(void (NS_NOESCAPE ^)(IRCChannel *channel, NSArray *nicknames))callbackBlock { NSParameterAssert(pasteboardData != nil); NSParameterAssert(callbackBlock != nil); /* This is a private method which means that we are very lazy about validating the input, but this is a TODO to myself: add strict type checks if you end up making this method public. */ NSDictionary *pasteboardDictionary = [NSKeyedUnarchiver unarchiveObjectWithData:pasteboardData]; if ([pasteboardDictionary isKindOfClass:[NSDictionary class]] == NO) { return NO; } NSString *channelId = pasteboardDictionary[@"channelId"]; IRCChannel *channel = (IRCChannel *)[worldController() findItemWithId:channelId]; if (channel == nil) { return NO; } NSArray *nicknames = pasteboardDictionary[@"nicknames"]; callbackBlock(channel, nicknames); return YES; } + (BOOL)readMembersFromPasteboardData:(NSData *)pasteboardData withBlock:(void (NS_NOESCAPE ^)(IRCChannel *channel, NSArray *members))callbackBlock { NSParameterAssert(pasteboardData != nil); NSParameterAssert(callbackBlock != nil); return [self readNicknamesFromPasteboardData:pasteboardData withBlock:^(IRCChannel *channel, NSArray *nicknames) { NSMutableArray *members = [NSMutableArray arrayWithCapacity:nicknames.count]; for (NSString *nickname in nicknames) { IRCChannelUser *member = [channel findMember:nickname]; if (member == nil) { continue; } [members addObject:member]; } callbackBlock(channel, [members copy]); }]; } #pragma mark - #pragma mark Search - (BOOL)memberExists:(NSString *)nickname { return ([self findMember:nickname] != nil); } - (nullable IRCChannelUser *)findMember:(NSString *)nickname { NSParameterAssert(nickname != nil); IRCUser *user = [self.client findUser:nickname]; if (user == nil) { return nil; } IRCChannelUser *member = [user userAssociatedWithChannel:self.channel]; if (member == nil) { return nil; } return member; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCChannelMemberListController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual and/or Codeux Software, nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannelPrivate.h" #import "IRCChannelMemberList.h" #import "IRCChannelMemberListControllerPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCChannelMemberList () - (void)assignController:(nullable IRCChannelMemberListController *)controller; @end @interface IRCChannelMemberListController () @property (nonatomic, weak) IRCChannelMemberList *memberList; @property (nonatomic, weak) IBOutlet NSTableView *tableView; @end @implementation IRCChannelMemberListController - (void)assignToChannel:(nullable IRCChannel *)channel { /* Modify table sources */ NSTableView *tableView = self.tableView; tableView.dataSource = (id)channel; tableView.delegate = (id)channel; /* Are we assigned? */ IRCChannelMemberList *oldList = self.memberList; if ( oldList) { [oldList assignController:nil]; } IRCChannelMemberList *newList = channel.memberInfo; if ( newList) { [newList assignController:self]; } /* It is acceptable and correct behavior to assign nil to -memberList when there is none so there is no reason to place this assignment in the condition above. */ self.memberList = newList; if (channel == nil || newList == nil) { self.content = @[]; } } - (void)replaceContents:(NSArray *)contents { NSParameterAssert(contents != nil); self.content = [contents mutableCopy]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCChannelMode.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual and/or Codeux Software, nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCISupportInfoPrivate.h" #import "IRCModeInfo.h" #import "IRCChannelModePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCChannelMode () @property (nonatomic, weak) IRCClient *client; @property (nonatomic, weak) IRCChannel *channel; @property (nonatomic, copy, readwrite) IRCChannelModeContainer *modes; @end @interface IRCChannelModeContainer () @property (nonatomic, weak) IRCISupportInfo *supportInfo; @property (nonatomic, strong) NSMutableDictionary *modeObjects; @property (readonly, copy) NSArray *unwantedModes; - (instancetype)initWithSupportInfo:(IRCISupportInfo *)supportInfo; @end @implementation IRCChannelMode - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ((self = [super init])) { self.client = channel.associatedClient; self.channel = channel; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self->_modes = [[IRCChannelModeContainer alloc] initWithSupportInfo:self.client.supportInfo]; } - (NSArray *)updateModes:(NSString *)modeString { NSParameterAssert(modeString != nil); IRCISupportInfo *supportInfo = self.client.supportInfo; NSArray *modes = [supportInfo parseModes:modeString]; [self.modes applyModes:modes]; return modes; } - (NSString *)getChangeCommand:(IRCChannelModeContainer *)modes { NSParameterAssert(modes != nil); NSDictionary *modesSetOld = self.modes.modes; NSDictionary *modesSetNew = modes.modes; NSMutableString *modeAddString = [NSMutableString string]; NSMutableString *modeRemoveString = [NSMutableString string]; NSMutableString *modeParamString = [NSMutableString string]; /* Look over the set of modes that are currently set. If a mode is present in that set, but not in the new set, then mark that mode for removal. */ [modesSetOld enumerateKeysAndObjectsUsingBlock:^(NSString *modeSymbol, IRCModeInfo *mode, BOOL *stop) { if (modesSetNew[modeSymbol] != nil) { return; } if (modeRemoveString.length == 0) { [modeRemoveString appendFormat:@"-%@", modeSymbol]; } else { [modeRemoveString appendString:modeSymbol]; } }]; /* Look over the new set of modes and compare them to the old set. If a mode has changed (check for equality), then perform action. */ [modesSetNew enumerateKeysAndObjectsUsingBlock:^(NSString *modeSymbol, IRCModeInfo *mode, BOOL *stop) { IRCModeInfo *modeOld = modesSetOld[modeSymbol]; if ([mode isEqual:modeOld]) { return; } if (mode.modeIsSet) { if (modeAddString.length == 0) { [modeAddString appendFormat:@"+%@", modeSymbol]; } else { [modeAddString appendString:modeSymbol]; } } else { if (modeRemoveString.length == 0) { [modeRemoveString appendFormat:@"-%@", modeSymbol]; } else { [modeRemoveString appendString:modeSymbol]; } } NSString *modeParameter = mode.modeParameter; if (modeParameter.length == 0) { return; } [modeParamString appendFormat:@" %@", modeParameter]; }]; return [NSString stringWithFormat:@"%@%@%@", modeRemoveString, modeAddString, modeParamString]; } - (void)clear { [self.modes clear]; } - (BOOL)modeIsDefined:(NSString *)modeSymbol { return [self.modes modeIsDefined:modeSymbol]; } - (nullable IRCModeInfo *)modeInfoFor:(NSString *)modeSymbol { return [self.modes modeInfoFor:modeSymbol]; } - (NSString *)stringWithMaskedPassword:(BOOL)maskPassword { NSMutableString *modeSetString = [NSMutableString string]; NSMutableString *modeParamString = [NSMutableString string]; NSDictionary *modes = self.modes.modes; NSArray *modesSorted = modes.sortedDictionaryKeys; for (NSString *modeSymbol in modesSorted) { IRCModeInfo *mode = modes[modeSymbol]; if (mode.modeIsSet == NO) { continue; } if (modeSetString.length == 0) { [modeSetString appendFormat:@"+%@", modeSymbol]; } else { [modeSetString appendString:modeSymbol]; } NSString *modeParameter = mode.modeParameter; if (modeParameter.length == 0) { continue; } if ([modeSymbol isEqualToString:@"k"] && maskPassword) { [modeParamString appendFormat:@" ******"]; } else { [modeParamString appendFormat:@" %@", modeParameter]; } } return [modeSetString stringByAppendingString:modeParamString]; } - (NSString *)string { return [self stringWithMaskedPassword:NO]; } - (NSString *)stringWithMaskedPassword { return [self stringWithMaskedPassword:YES]; } @end #pragma mark - @implementation IRCChannelModeContainer - (instancetype)initWithSupportInfo:(IRCISupportInfo *)supportInfo { NSParameterAssert(supportInfo != nil); if ((self = [super init])) { self.supportInfo = supportInfo; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.modeObjects = [NSMutableDictionary dictionary]; } - (void)clear { @synchronized(self.modeObjects) { [self.modeObjects removeAllObjects]; } } - (NSDictionary *)modes { @synchronized(self.modeObjects) { return [self.modeObjects copy]; } } - (NSArray *)unwantedModes { IRCISupportInfo *supportInfo = self.supportInfo; /* Using some random value in place of mode if there is none is easier than creating a mutable array with four if statements. */ return @[ /* ban */ (([supportInfo modeSymbolForList:IRCISupportInfoListTypeBan]) ?: @"not supported: b"), /* ban exception */ (([supportInfo modeSymbolForList:IRCISupportInfoListTypeBanException]) ?: @"not supported: e"), /* invite exception */ (([supportInfo modeSymbolForList:IRCISupportInfoListTypeInviteException]) ?: @"not supported: I"), /* quiet */ (([supportInfo modeSymbolForList:IRCISupportInfoListTypeQuiet]) ?: @"not supported: q") ]; } - (BOOL)modeIsPermitted:(NSString *)modeSymbol { NSParameterAssert(modeSymbol != nil); if ([self.unwantedModes containsObject:modeSymbol]) { return NO; } if ([self.supportInfo modeSymbolIsUserPrefix:modeSymbol]) { return NO; } return YES; } - (BOOL)modeIsDefined:(NSString *)modeSymbol { NSParameterAssert(modeSymbol != nil); return (self.modes[modeSymbol] != nil); } - (nullable IRCModeInfo *)modeInfoFor:(NSString *)modeSymbol { NSParameterAssert(modeSymbol != nil); @synchronized (self.modeObjects) { IRCModeInfo *mode = self.modeObjects[modeSymbol]; if (mode) { return mode; } if ([self modeIsPermitted:modeSymbol] == NO) { return nil; } mode = [[IRCModeInfo alloc] initWithModeSymbol:modeSymbol]; self.modeObjects[modeSymbol] = mode; return mode; } } - (void)applyModes:(NSArray *)modes { NSParameterAssert(modes != nil); for (IRCModeInfo *mode in modes) { [self changeMode:mode.modeSymbol modeIsSet:mode.modeIsSet modeParameter:mode.modeParameter]; } } - (void)changeMode:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet { [self changeMode:modeSymbol modeIsSet:modeIsSet modeParameter:nil]; } - (void)changeMode:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameter:(nullable NSString *)modeParameter { NSParameterAssert(modeSymbol != nil); IRCModeInfo *mode = [self modeInfoFor:modeSymbol]; if (mode == nil) { return; } IRCModeInfoMutable *modeMutable = [mode mutableCopy]; modeMutable.modeSymbol = modeSymbol; modeMutable.modeIsSet = modeIsSet; modeMutable.modeParameter = modeParameter; @synchronized (self.modeObjects) { self.modeObjects[modeSymbol] = [modeMutable copy]; } } - (id)copyWithZone:(nullable NSZone *)zone { IRCChannelModeContainer *object = [[IRCChannelModeContainer alloc] initWithSupportInfo:self.supportInfo]; /* modeObjects contain immutable objects which means we don't have to copy the objects themselves. */ /* If we ever modify IRCModeInfo internal logic to ever modify the contents of the object after initialization, then we should perform a deep copy here. */ object.modeObjects = [self.modeObjects mutableCopy]; return object; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCClient.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* A portion of this source file contains copyrighted work derived from one or more 3rd-party, open source projects. The use of this work is hereby acknowledged. */ /* This source file contains work that originated from the Chat Core framework of the Colloquy project. The source in question is in relation to the handling of SASL authentication requests. The license of the Chat Core project is as follows: This document can be found mirrored at the author's website: No actual copyright is presented in the license file or the actual source file in which this work was obtained so the work is assumed to be Copyright © 2000 - 2012 the Colloquy IRC Client ------- License ------- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "GCDAsyncSocketExtensions.h" #import "TPCApplicationInfo.h" #import "TPCPathInfo.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TPCResourceManager.h" #import "TPCThemeController.h" #import "TPCTheme.h" #import "THOPluginDispatcherPrivate.h" #import "THOPluginManagerPrivate.h" #import "THOPluginProtocol.h" #import "TLOEncryptionManagerPrivate.h" #import "TLOFileLoggerPrivate.h" #import "TLOInputHistoryPrivate.h" #import "TLOLocalization.h" #import "TLONotificationControllerPrivate.h" #import "TLOpenLink.h" #import "TLOSoundPlayer.h" #import "TLOSpeechSynthesizerPrivate.h" #import "TLOSpokenNotificationPrivate.h" #import "TLOTimer.h" #import "TXGlobalModelsPrivate.h" #import "TXMasterControllerPrivate.h" #import "TXMenuControllerPrivate.h" #import "TXWindowControllerPrivate.h" #import "TVCDockIconPrivate.h" #import "TVCLogControllerPrivate.h" #import "TVCLogControllerInlineMediaServicePrivate.h" #import "TVCLogControllerOperationQueuePrivate.h" #import "TVCLogRenderer.h" #import "TVCLogViewPrivate.h" #import "TVCMainWindowPrivate.h" #import "TVCMainWindowTextViewPrivate.h" #import "TVCServerListPrivate.h" #import "TDCAlert.h" #import "TDCChannelBanListSheetPrivate.h" #import "TDCFileTransferDialogPrivate.h" #import "TDCFileTransferDialogTransferControllerPrivate.h" #import "TDCServerChannelListDialogPrivate.h" #import "TDCServerHighlightListSheetPrivate.h" #import "IRC.h" #import "IRCAddressBook.h" #import "IRCAddressBookMatchCachePrivate.h" #import "IRCAddressBookUserTrackingPrivate.h" #import "IRCChannelConfig.h" #import "IRCChannelModePrivate.h" #import "IRCChannelUserPrivate.h" #import "IRCChannelPrivate.h" #import "IRCClientConfigPrivate.h" #import "IRCClientRequestedCommandsPrivate.h" #import "IRCColorFormatPrivate.h" #import "IRCConnectionPrivate.h" #import "IRCConnectionConfig.h" #import "IRCConnectionErrors.h" #import "IRCExtrasPrivate.h" #import "IRCHighlightLogEntryPrivate.h" #import "IRCHighlightMatchCondition.h" #import "IRCISupportInfoPrivate.h" #import "IRCMessagePrivate.h" #import "IRCMessageBatchPrivate.h" #import "IRCModeInfo.h" #import "IRCNumerics.h" #import "IRCSendingMessage.h" #import "IRCServerPrivate.h" #import "IRCTimerCommandPrivate.h" #import "IRCTreeItemPrivate.h" #import "IRCUserPrivate.h" #import "IRCUserRelationsPrivate.h" #import "IRCWorldPrivate.h" #import "IRCClientPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _autojoinDelayedWarningInterval 90 // max delay after identification is 10 so keep this above that #define _autojoinDelayedWarningMaxCount 3 #define _isonCheckInterval 30 #define _pingInterval 270 #define _pongCheckInterval 30 #define _reconnectInterval 20 #define _retryInterval 240 #define _timeoutInterval 360 #define _whoCheckInterval 120 NSString * const IRCClientConfigurationWasUpdatedNotification = @"IRCClientConfigurationWasUpdatedNotification"; NSString * const IRCClientChannelListWasModifiedNotification = @"IRCClientChannelListWasModifiedNotification"; NSString * const IRCClientWillConnectNotification = @"IRCClientWillConnectNotification"; NSString * const IRCClientDidConnectNotification = @"IRCClientDidConnectNotification"; NSString * const IRCClientWillSendQuitNotification = @"IRCClientWillSendQuitNotification"; NSString * const IRCClientWillDisconnectNotification = @"IRCClientWillDisconnectNotification"; NSString * const IRCClientDidDisconnectNotification = @"IRCClientDidDisconnectNotification"; NSString * const IRCClientUserNicknameChangedNotification = @"IRCClientUserNicknameChangedNotification"; @interface IRCClient () // Properties that are public in IRCClient.h @property (nonatomic, copy, readwrite) IRCClientConfig *config; @property (nonatomic, copy, readwrite, nullable) IRCServer *server; @property (nonatomic, strong, readwrite) IRCISupportInfo *supportInfo; @property (nonatomic, assign, readwrite) BOOL isAutojoined; @property (nonatomic, assign, readwrite) BOOL isAutojoining; @property (nonatomic, assign, readwrite) BOOL isConnecting; @property (nonatomic, assign, readwrite) BOOL isConnected; @property (nonatomic, assign, readwrite) BOOL isConnectedToZNC; @property (nonatomic, assign, readwrite) BOOL isLoggedIn; @property (nonatomic, assign, readwrite) BOOL isQuitting; @property (nonatomic, assign, readwrite) BOOL isDisconnecting; @property (nonatomic, assign, readwrite) BOOL isReconnecting; @property (nonatomic, assign, readwrite) BOOL isSecured; @property (nonatomic, assign, readwrite) BOOL userIsAway; @property (nonatomic, assign, readwrite) BOOL userIsIRCop; @property (nonatomic, assign, readwrite) BOOL userIsIdentifiedWithNickServ; @property (nonatomic, assign, readwrite) BOOL isWaitingForNickServ; @property (nonatomic, assign, readwrite) BOOL serverHasNickServ; @property (nonatomic, assign, readwrite) NSTimeInterval lastMessageReceived; @property (nonatomic, assign, readwrite) NSTimeInterval lastMessageServerTime; @property (nonatomic, assign, readwrite) ClientIRCv3SupportedCapability capabilities; @property (nonatomic, copy, readwrite) NSArray *cachedHighlights; @property (nonatomic, copy, readwrite, nullable) NSString *userHostmask; @property (nonatomic, copy, readwrite) NSString *userNickname; @property (nonatomic, copy, readwrite) NSString *serverAddress; @property (nonatomic, copy, readwrite, nullable) NSString *preAwayUserNickname; @property (nonatomic, assign, readwrite) NSUInteger logFileSessionCount; // Properties private @property (nonatomic, assign) BOOL configurationIsStale; @property (nonatomic, strong, nullable) IRCConnection *socket; @property (nonatomic, strong) IRCMessageBatchMessageContainer *batchMessages; @property (nonatomic, strong, nullable) TLOFileLogger *logFile; @property (nonatomic, strong) TLOTimer *autojoinTimer; @property (nonatomic, strong) TLOTimer *autojoinNextJoinTimer; @property (nonatomic, strong) TLOTimer *autojoinDelayedWarningTimer; @property (nonatomic, strong) TLOTimer *isonTimer; @property (nonatomic, strong) TLOTimer *pongTimer; @property (nonatomic, strong) TLOTimer *reconnectTimer; @property (nonatomic, strong) TLOTimer *retryTimer; @property (nonatomic, strong) TLOTimer *whoTimer; @property (nonatomic, assign) BOOL capabilityNegotiationIsPaused; @property (nonatomic, assign) BOOL invokingISONCommandForFirstTime; @property (nonatomic, assign) BOOL isTerminating; // Is being destroyed @property (nonatomic, assign) BOOL inWhoisResponse; @property (nonatomic, assign) BOOL inWhowasResponse; @property (nonatomic, assign) BOOL reconnectEnabled; @property (nonatomic, assign) BOOL reconnectEnabledBecauseOfSleepMode; @property (nonatomic, assign) BOOL timeoutWarningShownToUser; @property (nonatomic, assign) BOOL zncBouncerIsSendingCertificateInfo; @property (nonatomic, assign) BOOL zncBouncerIsPlayingBackHistory; @property (nonatomic, strong) NSMutableArray *capabilitiesPending; @property (nonatomic, assign) NSUInteger connectDelay; @property (nonatomic, assign) NSUInteger lastServerSelected; @property (nonatomic, assign) NSUInteger lastWhoRequestChannelListIndex; @property (nonatomic, assign) NSUInteger successfulConnects; @property (nonatomic, assign) NSUInteger tryingNicknameNumber; @property (nonatomic, assign) NSUInteger autojoinDelayedWarningCount; @property (nonatomic, copy, nullable) NSString *tryingNicknameSentNickname; @property (nonatomic, strong) NSMutableArray *channelListPrivate; @property (nonatomic, strong, nullable) NSMutableArray *channelsToAutojoin; @property (nonatomic, strong) IRCAddressBookMatchCache *addressBookMatchCache; @property (nonatomic, strong) IRCAddressBookUserTrackingContainer *trackedUsers; @property (nonatomic, strong) IRCClientRequestedCommands *requestedCommands; @property (nonatomic, strong) NSMutableDictionary *timedCommands; @property (nonatomic, strong) NSMutableDictionary *userListPrivate; @property (nonatomic, strong, nullable) NSMutableString *zncBouncerCertificateChainDataMutable; @property (nonatomic, copy, nullable) NSString *temporaryServerAddressOverride; @property (nonatomic, assign) uint16_t temporaryServerPortOverride; @property (readonly) BOOL isBrokenIRCd_aka_Twitch; @property (readonly) BOOL monitorAwayStatus; @property (readonly) BOOL supportsAdvancedTracking; @property (readonly, copy) NSArray *nickServSupportedNeedIdentificationTokens; @property (readonly, copy) NSArray *nickServSupportedSuccessfulIdentificationTokens; @property (nonatomic, strong, nullable) IRCChannel *rawDataLogQuery; @property (nonatomic, strong, nullable) IRCChannel *hiddenCommandResponsesQuery; @end @implementation IRCClient #pragma mark - #pragma mark Initialization - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithConfigDictionary:(NSDictionary *)dic { NSParameterAssert(dic != nil); IRCClientConfig *config = [[IRCClientConfig alloc] initWithDictionary:dic]; return [self initWithConfig:config]; } - (instancetype)initWithConfig:(IRCClientConfig *)config { NSParameterAssert(config != nil); if ((self = [super init])) { self.config = config; [self writePasswordsToKeychain]; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.batchMessages = [IRCMessageBatchMessageContainer new]; self.supportInfo = [[IRCISupportInfo alloc] initWithClient:self]; self.connectType = IRCClientConnectModeNormal; self.disconnectType = IRCClientDisconnectModeNormal; self.cachedHighlights = @[]; self.capabilitiesPending = [NSMutableArray array]; self.channelListPrivate = [NSMutableArray array]; self.timedCommands = [NSMutableDictionary dictionary]; self.userListPrivate = [NSMutableDictionary dictionary]; self.addressBookMatchCache = [[IRCAddressBookMatchCache alloc] initWithClient:self]; self.trackedUsers = [[IRCAddressBookUserTrackingContainer alloc] initWithClient:self]; self.requestedCommands = [IRCClientRequestedCommands new]; self.lastMessageServerTime = self.config.lastMessageServerTime; self.lastServerSelected = NSNotFound; self.autojoinTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onAutojoinTimer]; }]; self.autojoinNextJoinTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onAutojoinNextJoinTimer]; }]; self.autojoinDelayedWarningTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onAutojoinDelayedWarningTimer]; }]; self.isonTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onISONTimer]; }]; self.reconnectTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onReconnectTimer]; }]; self.retryTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onRetryTimer]; }]; self.pongTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onPongTimer]; }]; self.whoTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self onWhoTimer]; }]; [RZNotificationCenter() addObserver:self selector:@selector(willDestroyChannel:) name:IRCWorldWillDestroyChannelNotification object:nil]; } - (void)dealloc { [RZNotificationCenter() removeObserver:self]; [self.autojoinTimer stop]; [self.autojoinNextJoinTimer stop]; [self.autojoinDelayedWarningTimer stop]; [self.isonTimer stop]; [self.pongTimer stop]; [self.reconnectTimer stop]; [self.retryTimer stop]; [self.whoTimer stop]; self.autojoinTimer = nil; self.autojoinNextJoinTimer = nil; self.autojoinDelayedWarningTimer = nil; self.isonTimer = nil; self.pongTimer = nil; self.reconnectTimer = nil; self.retryTimer = nil; self.whoTimer = nil; self.addressBookMatchCache = nil; self.batchMessages = nil; self.cachedHighlights = nil; self.channelListPrivate = nil; self.channelsToAutojoin = nil; self.logFile = nil; self.socket = nil; self.supportInfo = nil; self.timedCommands = nil; self.trackedUsers = nil; self.requestedCommands = nil; self.userListPrivate = nil; [self cancelPerformRequests]; } - (void)updateConfig:(IRCClientConfig *)config { [self updateConfig:config updateSelection:YES]; } - (void)updateConfig:(IRCClientConfig *)config updateSelection:(BOOL)updateSelection { NSParameterAssert(config != nil); if (self.isTerminating) { return; } IRCClientConfig *currentConfig = self.config; if ([currentConfig isEqual:config]) { return; } if ([currentConfig.uniqueIdentifier isEqualToString:config.uniqueIdentifier] == NO) { LogToConsoleError("Tried to load configuration for incorrect client"); return; } self.config = config; /* Update channel list */ { NSMutableArray *channelListOld = [self.channelList mutableCopy]; NSMutableArray *channelListNew = [NSMutableArray array]; NSMutableArray *channelListNewNames = [NSMutableArray array]; NSArray *channelConfigurations = self.config.channelList; for (IRCChannelConfig *channelConfig in channelConfigurations) { /* Block duplicate channel names by maintaining array of names */ NSString *channelName = channelConfig.channelName; if ([channelListNewNames containsObject:channelName] == NO) { [channelListNewNames addObject:channelName]; } else { continue; } /* Check whether the channel exists in the current list of channels */ /* If it does not exist, then create it. Otherwise, update it. */ IRCChannel *channel = [self findChannel:channelConfig.channelName inList:channelListOld]; if (channel == nil) { channel = [worldController() createChannelWithConfig:channelConfig onClient:self add:NO adjust:NO reload:NO]; } else { [channel updateConfig:channelConfig fireChangedNotification:NO updateStoredChannelList:NO]; [channelListOld removeObjectIdenticalTo:channel]; } [channelListNew addObject:channel]; } /* Any channels left in the old array can be destroyed or if they are not a channel, then they can be reinserted because we do not care about private messages being updated above so they must be reinserted here. */ for (IRCChannel *channel in channelListOld) { if (channel.isChannel == NO) { [channelListNew addObject:channel]; } else { [worldController() destroyChannel:channel reload:NO]; } } /* Save updated channel list then safe its contents */ self.channelList = channelListNew; } /* Update server list */ { /* To update the server list, we first make a map of all existing servers in a dictionary with the key as the identifier and the object is the server itself. */ NSArray *serverListOld = currentConfig.serverList; NSMutableDictionary *serverListOldMap = [[NSMutableDictionary alloc] initWithCapacity:serverListOld.count]; for (IRCServer *server in serverListOld) { serverListOldMap[server.uniqueIdentifier] = server; } /* We then make a map of the new server list */ NSArray *serverListNew = self.config.serverList; NSMutableDictionary *serverListNewMap = [[NSMutableDictionary alloc] initWithCapacity:serverListNew.count]; for (IRCServer *server in serverListNew) { serverListNewMap[server.uniqueIdentifier] = server; } /* Record information about the current server (if any). */ IRCServer *serverInUse = self.server; NSString *uniqueIdentifierInUse = serverInUse.uniqueIdentifier; /* Enumerate old server list */ /* If an old server no longer appears in the new list of identifiers, then we destroy its keychain items. If the server is the active server, then we mark the keychain items to be destroyed later, incase they need to be reused by IRCClient. */ [serverListOldMap enumerateKeysAndObjectsUsingBlock:^(NSString *uniqueIdentifier, IRCServer *server, BOOL *stop) { if ([serverListNewMap containsKey:uniqueIdentifier]) { return; } if ([uniqueIdentifier isEqualToString:uniqueIdentifierInUse]) { serverInUse.destroyKeychainItemsDuringDealloc = YES; } else { [server destroyServerPasswordKeychainItem]; } }]; /* Enumerate new server list */ /* All servers in the new server list have their keychain item written. */ if (serverListNew.count == 0) { self.lastServerSelected = NSNotFound; } else { [serverListNewMap enumerateKeysAndObjectsUsingBlock:^(NSString *uniqueIdentifier, IRCServer *server, BOOL *stop) { [server writeServerPasswordToKeychain]; }]; } } /* -reloadItem will drop the views and reload them. */ /* We need to remember the selection because of this. */ if (updateSelection) { [self reloadServerListItems]; } /* Update navigation list */ [menuController() populateNavigationChannelList]; /* Write passwords to keychain */ [self writePasswordsToKeychain]; [self destroyServerPasswordKeychainItemAfterMigration]; /* Update main window title */ [mainWindow() updateTitleFor:self]; /* Rebuild list of users that are ignored and/or tracked */ [self clearAddressBookCache]; [self populateISONTrackedUsersList]; /* Post notification */ [RZNotificationCenter() postNotificationName:IRCClientConfigurationWasUpdatedNotification object:self]; } - (void)reloadServerListItems { mainWindow().ignoreOutlineViewSelectionChanges = YES; [mainWindowServerList() beginUpdates]; [mainWindowServerList() reloadItem:self reloadChildren:YES]; [mainWindowServerList() endUpdates]; [mainWindow() adjustSelection]; mainWindow().ignoreOutlineViewSelectionChanges = NO; } - (void)writePasswordsToKeychain { [self.config writeNicknamePasswordToKeychain]; [self.config writeProxyPasswordToKeychain]; } - (void)destroyServerPasswordKeychainItemAfterMigration { [self.config destroyServerPasswordKeychainItemAfterMigration]; } - (void)updateStoredConfiguration { if (self.configurationIsStale == NO) { return; } IRCClientConfigMutable *configMutable = [self.config mutableCopy]; configMutable.lastMessageServerTime = self.lastMessageServerTime; configMutable.sidebarItemExpanded = self.sidebarItemIsExpanded; self.config = configMutable; } - (void)updateStoredChannelList { /* Rebuild list of channel configurations */ NSMutableArray *channelList = [NSMutableArray array]; for (IRCChannel *channel in self.channelList) { if (channel.isUtility) { continue; } if (channel.isChannel == NO && [TPCPreferences rememberServerListQueryStates] == NO) { continue; } [channelList addObject:channel.config]; } /* Save list */ IRCClientConfigMutable *mutableConfig = [self.config mutableCopy]; mutableConfig.channelList = channelList; self.config = mutableConfig; /* Post notification */ [RZNotificationCenter() postNotificationName:IRCClientChannelListWasModifiedNotification object:self]; } - (NSDictionary *)configurationDictionary { [self updateStoredConfiguration]; return [self.config dictionaryValue]; } - (void)prepareForApplicationTermination { self.isTerminating = YES; LogToConsoleTerminationProgress("Preparing client: <%{public}@>", self.uniqueIdentifier); LogToConsoleTerminationProgress("[%{public}@] Closing dialogs", self.uniqueIdentifier); [self closeDialogs]; if (self.isConnecting || self.isConnected) { LogToConsoleTerminationProgress("[%{public}@] Performing disconnect", self.uniqueIdentifier); __weak IRCClient *weakSelf = self; self.disconnectCallback = ^{ [weakSelf prepareForApplicationTerminationPostflight]; }; [self quit]; return; } [self prepareForApplicationTerminationPostflight]; } - (void)prepareForApplicationTerminationPostflight { LogToConsoleTerminationProgress("[%{public}@] Closing log file", self.uniqueIdentifier); [self closeLogFile]; LogToConsoleTerminationProgress("[%{public}@] Removing unspoken messages from speech synthesizer", self.uniqueIdentifier); [self clearEventsToSpeak]; LogToConsoleTerminationProgress("[%{public}@] Emptying Address Book cache", self.uniqueIdentifier); [self clearAddressBookCache]; LogToConsoleTerminationProgress("[%{public}@] Removing all tracked users", self.uniqueIdentifier); [self clearTrackedUsers]; LogToConsoleTerminationProgress("[%{public}@] Preparing channels: %{public}ld", self.uniqueIdentifier, self.channelCount); for (IRCChannel *c in self.channelList) { [c prepareForApplicationTermination]; } LogToConsoleTerminationProgress("[%{public}@] Preparing view controller: <%{public}@>", self.uniqueIdentifier, self.viewController.uniqueIdentifier); [self.viewController prepareForApplicationTermination]; LogToConsoleTerminationProgress("[%{public}@] Decrementing client count", self.uniqueIdentifier); masterController().terminatingClientCount -= 1; } - (void)prepareForPermanentDestruction { self.isTerminating = YES; // [self disconnect]; // Disconnect is called by IRCWorld for us [self closeDialogs]; [self closeLogFile]; [self clearEventsToSpeak]; [self clearAddressBookCache]; [self clearTrackedUsers]; [self.config destroyNicknamePasswordKeychainItem]; [self.config destroyProxyPasswordKeychainItem]; [self destroyServerPasswordsKeychainItems]; for (IRCChannel *c in self.channelList) { [c prepareForPermanentDestruction]; } [[mainWindow() inputHistoryManager] destroy:self]; [self.viewController prepareForPermanentDestruction]; } - (void)closeDialogs { TDCServerChannelListDialog *channelListDialog = [self channelListDialog]; if (channelListDialog) { [channelListDialog close]; } NSArray *openWindows = [windowController() windowsFromWindowList:@[@"TDCChannelInviteSheet", @"TDCServerChangeNicknameSheet", @"TDCServerHighlightListSheet", @"TDCServerPropertiesSheet"]]; for (TDCSheetBase *windowObject in openWindows) { if ([windowObject.clientId isEqualToString:self.uniqueIdentifier]) { [windowObject close]; } } } - (void)preferencesChanged { for (IRCChannel *c in self.channelList) { [c preferencesChanged]; } if (self.monitorAwayStatus == NO) { [self resetAwayStatusForUsers]; } } - (void)willDestroyChannel:(NSNotification *)notification { IRCChannel *channel = notification.object; if (channel.associatedClient != self) { return; } [self zncPlaybackClearChannel:channel]; if (self.hiddenCommandResponsesQuery == channel) { self.hiddenCommandResponsesQuery = channel; } if (self.rawDataLogQuery == channel) { self.rawDataLogQuery = nil; } } - (id)copyWithZone:(nullable NSZone *)zone { /* Implement this method to allow client to be used as a dictionary key. */ return self; } #pragma mark - #pragma mark Servers - (void)enumerateServers:(void (NS_NOESCAPE ^)(IRCServer *server, NSUInteger index, BOOL *stop))block { [self.config.serverList enumerateObjectsUsingBlock:block]; } - (void)writeServerPasswordsToKeychain { [self enumerateServers:^(IRCServer *server, NSUInteger index, BOOL *stop) { [server writeServerPasswordToKeychain]; }]; } - (void)destroyServerPasswordsKeychainItems { [self enumerateServers:^(IRCServer *server, NSUInteger index, BOOL *stop) { [server destroyServerPasswordKeychainItem]; }]; } #pragma mark - #pragma mark Properties - (NSString *)description { return [NSString stringWithFormat:@"", self.networkNameAlt, self.serverAddress]; } - (NSString *)uniqueIdentifier { return self.config.uniqueIdentifier; } - (NSString *)name { return self.config.connectionName; } - (nullable NSString *)networkName { return self.supportInfo.networkNameFormatted; } - (NSString *)networkNameAlt { NSString *networkName = self.networkName; if (networkName) { return networkName; } return self.config.connectionName; } - (nullable NSString *)serverAddress { NSString *serverAddress = self.supportInfo.serverAddress; if (serverAddress) { return serverAddress; } NSString *serverAddressOnSocket = self.socket.config.serverAddress; if (serverAddressOnSocket) { return serverAddressOnSocket; } return self.server.serverAddress; } - (NSString *)userNickname { NSString *userNickname = self->_userNickname; if (userNickname) { return userNickname; } return self.config.nickname; } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (NSString *)encryptionAccountNameForLocalUser { return [sharedEncryptionManager() accountNameForUser:self.userNickname onClient:self]; } - (NSString *)encryptionAccountNameForUser:(NSString *)nickname { NSParameterAssert(nickname != nil); return [sharedEncryptionManager() accountNameForUser:nickname onClient:self]; } #endif - (TDCFileTransferDialog *)fileTransferController { return [TXSharedApplication sharedFileTransferDialog]; } - (BOOL)isReconnecting { return self.reconnectTimer.timerIsActive; } - (void)setSidebarItemIsExpanded:(BOOL)sidebarItemIsExpanded { /* This is a non-critical property that can be saved periodically */ if (self->_sidebarItemIsExpanded != sidebarItemIsExpanded) { self->_sidebarItemIsExpanded = sidebarItemIsExpanded; self.configurationIsStale = YES; [worldController() savePeriodically]; } } - (void)setLastMessageServerTime:(NSTimeInterval)lastMessageServerTime { /* This is a non-critical property that can be saved periodically */ if (self->_lastMessageServerTime != lastMessageServerTime) { self->_lastMessageServerTime = lastMessageServerTime; self.configurationIsStale = YES; [worldController() savePeriodically]; } } - (BOOL)isSecured { if (self.socket) { return self.socket.isSecured; } return NO; } - (nullable NSData *)zncBouncerCertificateChainData { /* If the data is still being processed, then return nil so that partial data is not returned. */ if (self.isConnectedToZNC == NO || self.zncBouncerIsSendingCertificateInfo || self.zncBouncerCertificateChainDataMutable == nil) { return nil; } return [self.zncBouncerCertificateChainDataMutable dataUsingEncoding:NSASCIIStringEncoding]; } - (BOOL)isBrokenIRCd_aka_Twitch { return [self.serverAddress hasSuffix:@".twitch.tv"]; } - (BOOL)supportsAdvancedTracking { return ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityMonitorCommand] || [self isCapabilityEnabled:ClientIRCv3SupportedCapabilityWatchCommand]); } - (BOOL)monitorAwayStatus { return ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityAwayNotify] || [TPCPreferences trackUserAwayStatusMaximumChannelSize] > 0); } - (nullable TVCLogLine *)lastLine { return self.viewController.lastLine; } #pragma mark - #pragma mark Standalone Utilities - (BOOL)messageIsFromMyself:(IRCMessage *)message { NSParameterAssert(message != nil); return [self nicknameIsMyself:message.senderNickname]; } - (BOOL)nicknameIsMyself:(NSString *)nickname { NSParameterAssert(nickname != nil); return [self.userNickname isEqualToStringIgnoringCase:nickname]; } - (BOOL)stringIsNickname:(NSString *)string { NSParameterAssert(string != nil); return ([string isHostmaskNicknameOn:self] && [string isChannelNameOn:self] == NO); } - (BOOL)stringIsChannelName:(NSString *)string { NSParameterAssert(string != nil); return [string isChannelNameOn:self]; } - (BOOL)stringIsChannelNameOrZero:(NSString *)string { NSParameterAssert(string != nil); return ([self stringIsChannelName:string] || [string isEqualToString:@"0"]); } - (NSArray *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet parameterString:(NSString *)parameterString { return [self compileListOfModeChangesForModeSymbol:modeSymbol modeIsSet:modeIsSet parameterString:parameterString characterSet:[NSCharacterSet whitespaceCharacterSet]]; } - (NSArray *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet parameterString:(NSString *)parameterString characterSet:(NSCharacterSet *)characterList { NSParameterAssert(parameterString != nil); NSParameterAssert(characterList != nil); NSArray *modeParameters = [parameterString componentsSeparatedByCharactersInSet:characterList]; return [self compileListOfModeChangesForModeSymbol:modeSymbol modeIsSet:modeIsSet modeParameters:modeParameters]; } - (NSArray *)compileListOfModeChangesForModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameters:(NSArray *)modeParameters { NSParameterAssert(modeSymbol.length == 1); NSParameterAssert(modeParameters != nil); if (modeParameters.count == 0) { return @[]; } NSMutableArray *listOfChanges = [NSMutableArray array]; NSMutableString *modeSetString = [NSMutableString string]; NSMutableString *modeParamString = [NSMutableString string]; NSUInteger numberOfEntries = 0; for (NSString *modeParameter in modeParameters) { if (modeParameter.length == 0) { continue; } if (modeSetString.length == 0) { if (modeIsSet) { [modeSetString appendFormat:@"+%@", modeSymbol]; } else { [modeSetString appendFormat:@"-%@", modeSymbol]; } } else { [modeSetString appendString:modeSymbol]; } [modeParamString appendFormat:@" %@", modeParameter]; numberOfEntries += 1; if (numberOfEntries == self.supportInfo.maximumModeCount) { numberOfEntries = 0; NSString *modeSetCombined = [modeSetString stringByAppendingString:modeParamString]; [listOfChanges addObject:modeSetCombined]; [modeSetString setString:@""]; [modeParamString setString:@""]; } } if (modeSetString.length > 0 && modeParamString.length > 0) { NSString *modeSetCombined = [modeSetString stringByAppendingString:modeParamString]; [listOfChanges addObject:modeSetCombined]; } return [listOfChanges copy]; } - (void)separateTargetsInString:(NSString *)targetString withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *targets))completionBlock { NSParameterAssert(targetString != nil); NSParameterAssert(completionBlock != nil); NSArray *targets = [targetString componentsSeparatedByString:@","]; completionBlock(targets); } - (void)enumerateTargetsInString:(NSString *)targetString withBlock:(void (^)(NSString *target, NSUInteger atIndex, NSUInteger ofTotal, BOOL *stop))block { NSParameterAssert(targetString != nil); NSParameterAssert(block != nil); NSArray *targets = [targetString componentsSeparatedByString:@","]; NSUInteger targetsCount = targets.count; [targets enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { block(object, index, targetsCount, stop); }]; } #pragma mark - #pragma mark Highlights - (void)clearCachedHighlights { self.cachedHighlights = @[]; } - (void)cacheHighlightInChannel:(IRCChannel *)channel withLogLine:(TVCLogLine *)logLine { NSParameterAssert(channel != nil); NSParameterAssert(logLine != nil); if ([TPCPreferences logHighlights] == NO) { return; } /* Create entry */ IRCHighlightLogEntryMutable *newEntry = [IRCHighlightLogEntryMutable new]; newEntry.clientId = self.uniqueIdentifier; newEntry.channelId = channel.uniqueIdentifier; newEntry.lineLogged = logLine; /* We insert at head so that latest is always on top. */ NSMutableArray *cachedHighlights = [self.cachedHighlights mutableCopy]; [cachedHighlights insertObject:[newEntry copy] atIndex:0]; self.cachedHighlights = cachedHighlights; /* Reload table if the window is open. */ TDCServerHighlightListSheet *highlightListSheet = [windowController() windowFromWindowList:@"TDCServerHighlightListSheet"]; if ([highlightListSheet.clientId isEqualToString:self.uniqueIdentifier] == NO) { return; } [highlightListSheet addEntry:self.cachedHighlights.firstObject]; } #pragma mark - #pragma mark Reachability - (void)noteReachabilityChanged:(BOOL)reachable { if (reachable) { return; } [self disconnectOnReachabilityChange]; } - (void)disconnectOnReachabilityChange { if (self.isLoggedIn == NO) { return; } if (self.config.performDisconnectOnReachabilityChange == NO) { return; } self.disconnectType = IRCClientDisconnectModeReachabilityChange; self.reconnectEnabled = YES; XRPerformBlockSynchronouslyOnMainQueue(^{ [self disconnect]; }); } #pragma mark - #pragma mark Channel Storage - (void)selectFirstChannelInChannelList { NSArray *channelList = self.channelList; if (channelList.count == 0) { return; } [mainWindow() select:channelList[0]]; } - (void)addChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); @synchronized(self.channelListPrivate) { if ([self.channelListPrivate containsObject:channel]) { return; } /* Add channels atop of the first non-channel (private message). Private messages can be add to the bottom of the array. */ if (channel.isChannel == NO) { [self.channelListPrivate addObject:channel]; } else { NSUInteger privateMessageIndex = [self.channelListPrivate indexOfObjectPassingTest:^BOOL(IRCChannel *object, NSUInteger index, BOOL *stop) { return (object.isChannel == NO); }]; if (privateMessageIndex == NSNotFound) { [self.channelListPrivate addObject:channel]; } else { [self.channelListPrivate insertObject:channel atIndex:privateMessageIndex]; } } [self updateStoredChannelList]; } } - (void)addChannel:(IRCChannel *)channel atPosition:(NSUInteger)position { NSParameterAssert(channel != nil); @synchronized(self.channelListPrivate) { if ([self.channelListPrivate containsObject:channel]) { return; } [self.channelListPrivate insertObject:channel atIndex:position]; [self updateStoredChannelList]; } } - (void)removeChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); @synchronized(self.channelListPrivate) { [self.channelListPrivate removeObjectIdenticalTo:channel]; [self updateStoredChannelList]; } } - (NSUInteger)indexOfChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); @synchronized (self.channelListPrivate) { return [self.channelListPrivate indexOfObject:channel]; } } - (NSUInteger)channelCount { @synchronized (self.channelListPrivate) { return self.channelListPrivate.count; } } - (NSArray *)channelList { @synchronized (self.channelListPrivate) { return [self.channelListPrivate copy]; } } - (void)setChannelList:(NSArray *)channelList { NSParameterAssert(channelList != nil); @synchronized (self.channelListPrivate) { [self.channelListPrivate removeAllObjects]; [self.channelListPrivate addObjectsFromArray:channelList]; [self updateStoredChannelList]; } } #pragma mark - #pragma mark IRCTreeItem - (BOOL)isClient { return YES; } - (BOOL)isActive { return self.isLoggedIn; } - (nullable IRCClient *)associatedClient { return self; } - (nullable IRCChannel *)associatedChannel { return nil; } - (NSUInteger)numberOfChildren { return self.channelCount; } - (nullable id)childAtIndex:(NSUInteger)index { return self.channelList[index]; } - (NSString *)label { return self.config.connectionName; } #pragma mark - #pragma mark Encoding - (nullable NSData *)convertToCommonEncoding:(NSString *)string { NSParameterAssert(string != nil); NSData *data = [string dataUsingEncoding:self.config.primaryEncoding allowLossyConversion:NO]; if (data == nil) { data = [string dataUsingEncoding:self.config.fallbackEncoding allowLossyConversion:NO]; if (data == nil) { data = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; } } if (data == nil) { LogToConsoleError("NSData encode failure"); LogStackTrace(); } return data; } - (nullable NSString *)convertFromCommonEncoding:(NSData *)data { NSParameterAssert(data != nil); NSString *string = [NSString stringWithBytes:data.bytes length:data.length encoding:self.config.primaryEncoding]; if (string == nil) { string = [NSString stringWithBytes:data.bytes length:data.length encoding:self.config.fallbackEncoding]; if (string == nil) { string = [NSString stringWithBytes:data.bytes length:data.length encoding:NSASCIIStringEncoding]; } } if (string == nil) { LogToConsoleError("NSData decode failure"); LogStackTrace(); } return string; } #pragma mark - #pragma mark Address Book - (NSArray *)findIgnoresForHostmask:(NSString *)hostmask { return [self.addressBookMatchCache findIgnoresForHostmask:hostmask]; } - (nullable IRCAddressBookEntry *)findUserTrackingAddressBookEntryForHostmask:(NSString *)hostmask { /* We chop off the nickname from the host and only use that in the matching to keep everything a little more consistent internally. */ NSString *nickname = hostmask.nicknameFromHostmask; if (nickname == nil) { return nil; } return [self findUserTrackingAddressBookEntryForNickname:nickname]; } - (nullable IRCAddressBookEntry *)findUserTrackingAddressBookEntryForNickname:(NSString *)nickname { NSParameterAssert(nickname != nil); NSString *hostmask = [NSString stringWithFormat:@"%@!*@*", nickname]; return [self findAddressBookEntryForHostmask:hostmask]; } - (nullable IRCAddressBookEntry *)findAddressBookEntryForHostmask:(NSString *)hostmask { return [self.addressBookMatchCache findAddressBookEntryForHostmask:hostmask]; } - (void)clearAddressBookCache { [self.addressBookMatchCache clearCachedMatches]; } - (void)clearAddressBookCacheForHostmask:(NSString *)hostmask { /* Clear host */ [self.addressBookMatchCache clearCachedMatchesForHostmask:hostmask]; /* Clear nickname */ NSString *nickname = hostmask.nicknameFromHostmask; NSString *nicknameHostmask = [NSString stringWithFormat:@"%@!*@*", nickname]; [self.addressBookMatchCache clearCachedMatchesForHostmask:nicknameHostmask]; } #pragma mark - #pragma mark Output Rules - (BOOL)outputRuleMatchedInMessage:(NSString *)message inChannel:(nullable IRCChannel *)channel { NSParameterAssert(message != nil); if ([TPCPreferences removeAllFormatting] == NO) { message = message.stripIRCEffects; } NSArray *rules = sharedPluginManager().pluginOutputSuppressionRules; for (THOPluginOutputSuppressionRule *rule in rules) { if ([XRRegularExpression string:message isMatchedByRegex:rule.match] == NO) { continue; } if (channel) { if ((channel.isChannel && rule.restrictChannel) || (channel.isPrivateMessage && rule.restrictPrivateMessage)) { return YES; } } else { if (rule.restrictConsole) { return YES; } } } return NO; } #pragma mark - #pragma mark Encryption and Decryption - (NSDictionary *)listOfNicknamesToDisallowEncryption { return [TPCResourceManager dictionaryFromResources:@"StaticStore" key:@"IRCClient List of Nicknames that Encryption Forbids"]; } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (BOOL)encryptionAllowedForTarget:(NSString *)target { return [self encryptionAllowedForTarget:target lenient:NO]; } - (BOOL)encryptionAllowedForTarget:(NSString *)target lenient:(BOOL)lenient { NSParameterAssert(target != nil); /* Encryption is disabled */ if ([TPCPreferences textEncryptionIsEnabled] == NO) { return NO; } /* General rules */ if ([self stringIsNickname:target] == NO) { // Do not allow channel names return NO; } else if ([self nicknameIsMyself:target] && lenient == NO) { // Do not allow the local user return NO; } else if ([self nicknameIsZNCUser:target] && lenient == NO) { // Do not allow a ZNC private user return NO; } /* Build context information for lookup */ NSDictionary *exceptionRules = [self listOfNicknamesToDisallowEncryption]; NSString *lowercaseNickname = target.lowercaseString; /* Check network specific rules (such as "X" on UnderNet) */ NSString *networkName = self.supportInfo.networkName; if (networkName) { NSArray *networkSpecificData = [exceptionRules arrayForKey:networkName]; if ([networkSpecificData containsObject:lowercaseNickname]) { return NO; } } /* Look up rules for all networks */ NSArray *defaultsData = exceptionRules[@"-default-"]; if ([defaultsData containsObject:lowercaseNickname]) { return NO; } /* Allow the nickname through when there are no rules */ return YES; } #endif - (NSUInteger)lengthOfEncryptedMessageDirectedAt:(NSString *)messageTo thatFitsWithinBounds:(NSUInteger)maximumLength { return 0; } - (void)encryptMessage:(NSString *)messageBody directedAt:(NSString *)messageTo encodingCallback:(TLOEncryptionManagerEncodingDecodingCallbackBlock)encodingCallback injectionCallback:(TLOEncryptionManagerInjectCallbackBlock)injectionCallback { NSParameterAssert(messageBody != nil); NSParameterAssert(messageTo != nil); NSParameterAssert(encodingCallback != nil); NSParameterAssert(injectionCallback != nil); #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 /* Check if we are accepting encryption from this user */ if (messageBody.length == 0 || [self encryptionAllowedForTarget:messageTo] == NO) { #endif if (encodingCallback) { encodingCallback(messageBody, NO); } if (injectionCallback) { injectionCallback(messageBody); } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 return; } /* Continue with normal encryption operations */ [sharedEncryptionManager() encryptMessage:messageBody from:[self encryptionAccountNameForLocalUser] to:[self encryptionAccountNameForUser:messageTo] encodingCallback:encodingCallback injectionCallback:injectionCallback]; #endif } - (void)decryptMessage:(NSString *)messageBody from:(NSString *)messageFrom target:(NSString *)target decodingCallback:(TLOEncryptionManagerEncodingDecodingCallbackBlock)decodingCallback { NSParameterAssert(messageBody != nil); NSParameterAssert(messageFrom != nil); NSParameterAssert(target != nil); NSParameterAssert(decodingCallback != nil); #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 /* Check if we are accepting encryption from this user */ if (messageBody.length == 0 || [self encryptionAllowedForTarget:target lenient:YES] == NO) { #endif if (decodingCallback) { decodingCallback(messageBody, NO); } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 return; } /* Continue with normal encryption operations */ [sharedEncryptionManager() decryptMessage:messageBody from:[self encryptionAccountNameForUser:messageFrom] to:[self encryptionAccountNameForLocalUser] decodingCallback:decodingCallback]; #endif } - (void)encryptionAuthenticateUser:(NSString *)nickname { NSParameterAssert(nickname != nil); #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 /* Encryption is disabled */ if ([TPCPreferences textEncryptionIsEnabled] == NO) { return; } /* General rules */ if ([self stringIsNickname:nickname] == NO) { return; } if ([self nicknameIsMyself:nickname]) { return; } /* Authenticate user */ [sharedEncryptionManager() authenticateUser:[self encryptionAccountNameForUser:nickname] from:[self encryptionAccountNameForLocalUser]]; #endif } #pragma mark - #pragma mark Notifications - (nullable NSString *)formatNotificationToSpeak:(TLOSpokenNotification *)notification { NSParameterAssert(notification != nil); if (self.isTerminating) { return nil; } NSString *formattedMessage = nil; TXNotificationType eventType = notification.notificationType; IRCChannel *channel = notification.channel; NSString *nickname = notification.nickname; NSString *text = notification.text; if (text) { text = text.trim; if ([TPCPreferences removeAllFormatting] == NO) { text = text.stripIRCEffects; } } switch (eventType) { case TXNotificationTypeHighlight: { NSParameterAssert(channel != nil); NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); if (text.length == 0) { break; } /* Highlights are spoken regardless of whether the user has configured Channel Messages to be only spoken for selection. When the user has configured that preference, then we exclude the channel name at least because that information is uninteresting. */ /* For private messages, we speak everything, regardless of any preference. */ BOOL isChannel = channel.isChannel; BOOL onlySpeakEventsForSelection = [TPCPreferences onlySpeakEventsForSelection]; BOOL speakChannelName = /* 1 */ (isChannel == NO || /* 2 */ (onlySpeakEventsForSelection == NO && [TPCPreferences channelMessageSpeakChannelName]) || /* 2 */ (onlySpeakEventsForSelection && [mainWindow() isItemSelected:channel] == NO)); BOOL speakNickname = (isChannel == NO || [TPCPreferences channelMessageSpeakNickname]); NSMutableString *mutableMessage = [NSMutableString string]; [mutableMessage appendString:TXTLS(@"Notifications[qld-sa]")]; if (speakChannelName || speakNickname) { if (speakChannelName) { if (isChannel) { [mutableMessage appendString:TXTLS(@"Notifications[ke8-17]", channel.name.channelNameWithoutBang)]; // Channel } else { [mutableMessage appendString:TXTLS(@"Notifications[6nw-ec]")]; // Private Message } } if (speakNickname) { if (isChannel) { [mutableMessage appendString:TXTLS(@"Notifications[qy9-86]", nickname)]; // by } else { [mutableMessage appendString:TXTLS(@"Notifications[a7i-pu]", nickname)]; // from } } [mutableMessage appendString:TXTLS(@"Notifications[g9t-bt]")]; } [mutableMessage appendString:text]; formattedMessage = [mutableMessage copy]; break; } case TXNotificationTypeChannelMessage: case TXNotificationTypeChannelNotice: { NSParameterAssert(channel != nil); NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); if (text.length == 0) { break; } BOOL onlySpeakEventsForSelection = [TPCPreferences onlySpeakEventsForSelection]; BOOL channelIsSelected = [mainWindow() isItemSelected:channel]; if (onlySpeakEventsForSelection && channelIsSelected == NO) { break; } BOOL speakChannelName = (onlySpeakEventsForSelection == NO && [TPCPreferences channelMessageSpeakChannelName]); BOOL speakNickname = [TPCPreferences channelMessageSpeakNickname]; NSMutableString *mutableMessage = [NSMutableString string]; if (speakChannelName || speakNickname) { if (eventType == TXNotificationTypeChannelMessage) { [mutableMessage appendString:TXTLS(@"Notifications[tao-i2]")]; } else if (eventType == TXNotificationTypeChannelNotice) { [mutableMessage appendString:TXTLS(@"Notifications[kk4-68]")]; } if (speakChannelName) { [mutableMessage appendString:TXTLS(@"Notifications[ke8-17]", channel.name.channelNameWithoutBang)]; } if (speakNickname) { [mutableMessage appendString:TXTLS(@"Notifications[qy9-86]", nickname)]; } [mutableMessage appendString:TXTLS(@"Notifications[g9t-bt]")]; } [mutableMessage appendString:text]; formattedMessage = [mutableMessage copy]; break; } case TXNotificationTypeNewPrivateMessage: case TXNotificationTypePrivateMessage: case TXNotificationTypePrivateNotice: { NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); if (text.length == 0) { break; } NSString *formatter = nil; if (eventType == TXNotificationTypeNewPrivateMessage) { formatter = @"Notifications[rvb-9l]"; } else if (eventType == TXNotificationTypePrivateMessage) { formatter = @"Notifications[2bu-ep]"; } else if (eventType == TXNotificationTypePrivateNotice) { formatter = @"Notifications[6jl-vh]"; } formattedMessage = TXTLS(formatter, nickname, text); break; } case TXNotificationTypeKick: { NSParameterAssert(channel != nil); NSParameterAssert(nickname != nil); NSString *formatter = @"Notifications[5yu-bf]"; formattedMessage = TXTLS(formatter, channel.name.channelNameWithoutBang, nickname); break; } case TXNotificationTypeInvite: { NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); NSString *formatter = @"Notifications[l5i-at]"; formattedMessage = TXTLS(formatter, text.channelNameWithoutBang, nickname); break; } case TXNotificationTypeConnect: case TXNotificationTypeDisconnect: { NSString *formatter = nil; if (eventType == TXNotificationTypeConnect) { formatter = @"Notifications[z4p-yr]"; } else if (eventType == TXNotificationTypeDisconnect) { formatter = @"Notifications[fd0-f8]"; } formattedMessage = TXTLS(formatter, self.networkNameAlt); break; } case TXNotificationTypeAddressBookMatch: { NSParameterAssert(text != nil); formattedMessage = text; break; } case TXNotificationTypeFileTransferSendSuccessful: case TXNotificationTypeFileTransferReceiveSuccessful: case TXNotificationTypeFileTransferSendFailed: case TXNotificationTypeFileTransferReceiveFailed: case TXNotificationTypeFileTransferReceiveRequested: { NSParameterAssert(nickname != nil); NSString *formatter = nil; if (eventType == TXNotificationTypeFileTransferSendSuccessful) { formatter = @"Notifications[5e4-vg]"; } else if (eventType == TXNotificationTypeFileTransferReceiveSuccessful) { formatter = @"Notifications[4cd-p3]"; } else if (eventType == TXNotificationTypeFileTransferSendFailed) { formatter = @"Notifications[f0u-32]"; } else if (eventType == TXNotificationTypeFileTransferReceiveFailed) { formatter = @"Notifications[mak-bj]"; } else if (eventType == TXNotificationTypeFileTransferReceiveRequested) { formatter = @"Notifications[at0-vi]"; } formattedMessage = TXTLS(formatter, nickname); break; } case TXNotificationTypeUserJoined: case TXNotificationTypeUserParted: { NSParameterAssert(channel != nil); NSParameterAssert(nickname != nil); NSString *formatter = nil; if (eventType == TXNotificationTypeUserJoined) { formatter = @"Notifications[bwu-ps]"; } else if (eventType == TXNotificationTypeUserParted) { formatter = @"Notifications[4aq-hz]"; } formattedMessage = TXTLS(formatter, nickname, channel.name.channelNameWithoutBang); break; } case TXNotificationTypeUserDisconnected: { NSParameterAssert(nickname != nil); NSString *formatter = @"Notifications[sqf-4y]"; formattedMessage = TXTLS(formatter, nickname); break; } } return formattedMessage; } - (void)clearEventsToSpeak { [[TXSharedApplication sharedSpeechSynthesizer] clearQueueForClient:self]; } - (void)speakEvent:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType target:(null_unspecified IRCTreeItem *)target nickname:(null_unspecified NSString *)nickname text:(null_unspecified NSString *)text { if ([sharedNotificationController() speakEvent:eventType inChannel:(IRCChannel *)target] == NO) { return; } if (target == nil) { target = self; } TLOSpokenNotification *notification = [[TLOSpokenNotification alloc] initWithNotification:eventType lineType:lineType target:target nickname:nickname text:text]; [[TXSharedApplication sharedSpeechSynthesizer] speak:notification]; } - (BOOL)notifyText:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType target:(IRCChannel *)target nickname:(NSString *)nickname text:(NSString *)text { return [self notifyEvent:eventType lineType:lineType target:target nickname:nickname text:text userInfo:nil]; } - (BOOL)notifyEvent:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType { return [self notifyEvent:eventType lineType:lineType target:nil nickname:nil text:nil userInfo:nil]; } - (BOOL)notifyEvent:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType target:(null_unspecified IRCChannel *)target nickname:(null_unspecified NSString *)nickname text:(null_unspecified NSString *)text { return [self notifyEvent:eventType lineType:lineType target:target nickname:nickname text:text userInfo:nil]; } - (BOOL)notifyEvent:(TXNotificationType)eventType lineType:(TVCLogLineType)lineType target:(null_unspecified IRCChannel *)target nickname:(null_unspecified NSString *)nickname text:(null_unspecified NSString *)text userInfo:(nullable NSDictionary *)userInfo { if (self.isTerminating) { return NO; } BOOL isTextEvent = (eventType == TXNotificationTypeHighlight || eventType == TXNotificationTypeNewPrivateMessage || eventType == TXNotificationTypeChannelMessage || eventType == TXNotificationTypeChannelNotice || eventType == TXNotificationTypePrivateMessage || eventType == TXNotificationTypePrivateNotice); if (isTextEvent) { if ([self nicknameIsMyself:nickname]) { return NO; } } if (target && text != nil) { if ([self outputRuleMatchedInMessage:text inChannel:target]) { return NO; } } IRCChannelConfig *targetConfig = nil; if (target) { targetConfig = target.config; if (eventType == TXNotificationTypeHighlight) { if (targetConfig.ignoreHighlights) { return YES; } } else { if (targetConfig.pushNotifications == NO) { return YES; } } } if ([sharedNotificationController() bounceDockIconForEvent:eventType inChannel:target]) { if ([sharedNotificationController() bounceDockIconRepeatedlyForEvent:eventType inChannel:target]) { [NSApp requestUserAttention:NSCriticalRequest]; } else { [NSApp requestUserAttention:NSInformationalRequest]; } } if (sharedNotificationController().areNotificationsDisabled) { return YES; } BOOL mainWindowIsFocused = (mainWindow().inactive == NO); BOOL postNotificationsWhileFocused = [TPCPreferences postNotificationsWhileInFocus]; BOOL targetIsSelected = [mainWindow() isItemSelected:target]; BOOL onlySpeakEvent = (postNotificationsWhileFocused && mainWindowIsFocused && targetIsSelected); if ([TPCPreferences soundIsMuted] == NO) { if (onlySpeakEvent == NO) { NSString *soundName = [sharedNotificationController() soundForEvent:eventType inChannel:target]; if (soundName) { [TLOSoundPlayer playAlertSound:soundName]; } } [self speakEvent:eventType lineType:lineType target:target nickname:nickname text:text]; } if (onlySpeakEvent) { return YES; } if ([sharedNotificationController() notificationEnabledForEvent:eventType inChannel:target] == NO) { return YES; } if (postNotificationsWhileFocused == NO && mainWindowIsFocused) { if (eventType != TXNotificationTypeAddressBookMatch) { return YES; } } if ([sharedNotificationController() disabledWhileAwayForEvent:eventType inChannel:target]) { if (self.userIsAway) { return YES; } } NSString *eventTitle = nil; NSString *eventDescription = nil; if (userInfo == nil) { if (target) { userInfo = @{TXNotificationUserInfoClientIdentifierKey : self.uniqueIdentifier, TXNotificationUserInfoChannelIdentifierKey: target.uniqueIdentifier}; } else { userInfo = @{TXNotificationUserInfoClientIdentifierKey : self.uniqueIdentifier}; } } switch (eventType) { case TXNotificationTypeHighlight: case TXNotificationTypeNewPrivateMessage: case TXNotificationTypeChannelMessage: case TXNotificationTypeChannelNotice: case TXNotificationTypePrivateMessage: case TXNotificationTypePrivateNotice: { NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); if (eventType == TXNotificationTypeHighlight || eventType == TXNotificationTypeChannelMessage || eventType == TXNotificationTypeChannelNotice) { NSParameterAssert(target != nil); eventTitle = target.name; } if (lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeActionNoHighlight) { eventDescription = [NSString stringWithFormat:TXNotificationDialogActionNicknameFormat, nickname, text]; } else { nickname = [self formatNickname:nickname inChannel:target]; eventDescription = [NSString stringWithFormat:TXNotificationDialogStandardNicknameFormat, nickname, text]; } break; } case TXNotificationTypeFileTransferSendSuccessful: case TXNotificationTypeFileTransferReceiveSuccessful: case TXNotificationTypeFileTransferSendFailed: case TXNotificationTypeFileTransferReceiveFailed: case TXNotificationTypeFileTransferReceiveRequested: { NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); eventTitle = nickname; eventDescription = text; break; } case TXNotificationTypeConnect: { eventTitle = self.networkNameAlt; break; } case TXNotificationTypeDisconnect: { eventTitle = self.networkNameAlt; break; } case TXNotificationTypeAddressBookMatch: { NSParameterAssert(text != nil); eventDescription = text; break; } case TXNotificationTypeKick: { NSParameterAssert(target != nil); NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); eventTitle = self.networkNameAlt; eventDescription = TXTLS(@"Notifications[fkt-p3]", nickname, target.name, text); break; } case TXNotificationTypeInvite: { NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); eventTitle = self.networkNameAlt; eventDescription = TXTLS(@"Notifications[xl5-dn]", nickname, text); break; } case TXNotificationTypeUserJoined: { NSParameterAssert(target != nil); NSParameterAssert(nickname != nil); eventTitle = self.networkNameAlt; eventDescription = TXTLS(@"Notifications[yas-us]", nickname, target.name); break; } case TXNotificationTypeUserParted: { NSParameterAssert(target != nil); NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); eventTitle = self.networkNameAlt; if (text == nil || text.length == 0) { eventDescription = TXTLS(@"Notifications[bu2-9m]", nickname, target.name); } else { eventDescription = TXTLS(@"Notifications[3ur-i8]", nickname, target.name, text); } break; } case TXNotificationTypeUserDisconnected: { NSParameterAssert(nickname != nil); NSParameterAssert(text != nil); eventTitle = self.networkNameAlt; if (text == nil || text.length == 0) { eventDescription = TXTLS(@"Notifications[7ao-n8]", nickname); } else { eventDescription = TXTLS(@"Notifications[ssw-m6]", nickname, text); } break; } default: { return YES; } } [sharedNotificationController() notify:eventType title:eventTitle description:eventDescription userInfo:userInfo]; return YES; } #pragma mark - #pragma mark Playback - (void)playbackClearChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityPlayback] == NO) { return; } if (channel.isPrivateMessage == NO || channel.isPrivateMessageForZNCUser) { return; } NSString *command = [NSString stringWithFormat:@"clear %@", channel.name]; if (self.isConnectedToZNC) { [self sendCommand:command toZNCModuleNamed:@"playback"]; return; } [self send:@"PRIVMSG", @"*playback", command, nil]; } - (void)requestPlayback { if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityPlayback] == NO) { return; } /* For our first connect, only playback using timestamp if logging was enabled. */ /* For all other connects, then playback timestamp regardless of logging. */ NSString *command = nil; if ((self.successfulConnects > 1 || (self.successfulConnects == 1 && self.config.zncOnlyPlaybackLatest)) && self.lastMessageServerTime > 0) { command = [NSString stringWithFormat:@"play * %.0f", self.lastMessageServerTime]; } else { command = @"play * 0"; } if (self.isConnectedToZNC) { [self sendCommand:command toZNCModuleNamed:@"playback"]; return; } [self send:@"PRIVMSG", @"*playback", command, nil]; } #pragma mark - #pragma mark ZNC Bouncer Accessories - (void)zncPlaybackClearChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if (self.isConnectedToZNC == NO) { return; } [self playbackClearChannel:channel]; } - (BOOL)nicknameIsZNCUser:(NSString *)nickname { NSParameterAssert(nickname != nil); if (self.isConnectedToZNC == NO) { return NO; } return [nickname hasPrefix:@"*"]; } - (BOOL)nickname:(NSString *)nickname isZNCUser:(NSString *)zncNickname { NSParameterAssert(nickname != nil); NSParameterAssert(zncNickname != nil); return [nickname isEqualToString:[self nicknameAsZNCUser:zncNickname]]; } - (nullable NSString *)nicknameAsZNCUser:(NSString *)nickname { NSParameterAssert(nickname != nil); if (self.isConnectedToZNC == NO) { return nil; } return [@"*" stringByAppendingString:nickname]; } - (BOOL)isSafeToPostNotificationForMessage:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel { NSParameterAssert(message != nil); if (self.isConnectedToZNC == NO) { return YES; } if (self.config.zncIgnoreUserNotifications) { if (channel && [self nicknameIsZNCUser:channel.name]) { return NO; } } if (self.config.zncIgnorePlaybackNotifications == NO) { return YES; } if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityBatch]) { NSString *batchType = message.parentBatchMessage.batchType; return ([batchType isEqualToString:@"znc.in/playback"] == NO); } return (message.isHistoric == NO); } - (void)updateConnectedToZNCPropertyWithMessage:(IRCMessage *)message { NSParameterAssert(message != nil); if (self.isConnectedToZNC) { return; } if (message.senderIsServer == NO) { return; } if ([message.senderNickname isEqualToString:@"irc.znc.in"]) { self.isConnectedToZNC = YES; LogToConsole("ZNC detected..."); } } - (void)sendCommand:(NSString *)command toZNCModuleNamed:(NSString *)module { NSParameterAssert(command != nil); NSParameterAssert(module != nil); NSString *destination = [self nicknameAsZNCUser:module]; if (destination == nil) { return; } NSString *stringToSend = [NSString stringWithFormat:@"ZNC %@ %@", destination, command]; [self sendLine:stringToSend]; } #pragma mark - #pragma mark Channel States - (void)setHighlightStateForChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if (mainWindow().keyWindow && [mainWindow() isItemSelected:channel]) { return; } channel.nicknameHighlightCount += 1; [TVCDockIcon updateDockIcon]; [mainWindow() reloadTreeItem:channel]; } - (void)setUnreadStateForChannel:(IRCChannel *)channel { [self setUnreadStateForChannel:channel isHighlight:NO]; } - (void)setUnreadStateForChannel:(IRCChannel *)channel isHighlight:(BOOL)isHighlight { NSParameterAssert(channel != nil); if (mainWindow().keyWindow && [mainWindow() isItemSelected:channel]) { return; } if (channel.isChannel == NO || [TPCPreferences displayPublicMessageCountOnDockBadge]) { channel.dockUnreadCount += 1; [TVCDockIcon updateDockIcon]; } channel.treeUnreadCount += 1; // The isHighlight flag is not sent for the purpose of incrementing // a count. It's passed so that we can know whether the option to // show badge count should be ignored when performing update. if (isHighlight || channel.config.showTreeBadgeCount) { [mainWindowServerList() refreshMessageCountForItem:channel]; } } #pragma mark - #pragma mark Find Channel - (nullable IRCChannel *)findChannel:(NSString *)withName inList:(NSArray *)channelList { NSParameterAssert(withName != nil); NSParameterAssert(channelList != nil); NSUInteger channelIndex = [channelList indexOfObjectWithOptions:NSEnumerationConcurrent passingTest:^BOOL(IRCChannel *channel, NSUInteger index, BOOL *stop) { NSString *channelName = channel.name; return [withName isEqualToStringIgnoringCase:channelName]; }]; if (channelIndex != NSNotFound) { return channelList[channelIndex]; } return nil; } - (nullable IRCChannel *)findChannel:(NSString *)name { return [self findChannel:name inList:self.channelList]; } - (nullable IRCChannel *)findChannelOrCreate:(NSString *)name { return [self findChannelOrCreate:name isPrivateMessage:NO]; } - (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName isPrivateMessage:(BOOL)isPrivateMessage { NSParameterAssert(withName != nil); if (isPrivateMessage == NO) { return [self findChannelOrCreate:withName asType:IRCChannelTypeChannel]; } else { return [self findChannelOrCreate:withName asType:IRCChannelTypePrivateMessage]; } } - (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName isUtility:(BOOL)isUtility { NSParameterAssert(withName != nil); if (isUtility == NO) { return [self findChannelOrCreate:withName asType:IRCChannelTypeChannel]; } else { return [self findChannelOrCreate:withName asType:IRCChannelTypeUtility]; } } - (nullable IRCChannel *)findChannelOrCreate:(NSString *)withName asType:(IRCChannelType)type { NSParameterAssert(withName != nil); IRCChannel *channel = [self findChannel:withName]; if (channel) { return channel; } if (type == IRCChannelTypeChannel) { IRCChannelConfig *config = [IRCChannelConfig seedWithName:withName]; channel = [worldController() createChannelWithConfig:config onClient:self add:YES adjust:YES reload:YES]; [worldController() savePeriodically]; } else { channel = [worldController() createPrivateMessage:withName onClient:self asType:type]; } return channel; } #pragma mark - #pragma mark User List - (nullable IRCUser *)myself { return [self findUser:self.userNickname]; } - (BOOL)userExists:(NSString *)nickname { NSParameterAssert(nickname != nil); return ([self findUser:nickname] != nil); } - (nullable IRCUser *)findUser:(NSString *)nickname { NSParameterAssert(nickname != nil); nickname = nickname.lowercaseString; @synchronized (self.userListPrivate) { return self.userListPrivate[nickname]; } } - (IRCUserMutable *)mutableCopyOfUserWithNickname:(NSString *)nickname { NSParameterAssert(nickname != nil); IRCUser *user = [self findUser:nickname]; if (user == nil) { return [[IRCUserMutable alloc] initWithNickname:nickname onClient:self]; } else { return [user mutableCopy]; } } - (NSUInteger)numberOfUsers { @synchronized (self.userListPrivate) { return self.userListPrivate.count; } } - (NSArray *)userList { @synchronized (self.userListPrivate) { return self.userListPrivate.allValues; } } - (void)addUser:(IRCUser *)user { [self addUserAndReturn:user]; } - (IRCUser *)addUserAndReturn:(IRCUser *)user { NSParameterAssert(user != nil); if ([user isKindOfClass:[IRCUserMutable class]]) { user = [user copy]; } NSString *nickname = user.lowercaseNickname; @synchronized (self.userListPrivate) { self.userListPrivate[nickname] = user; } [user becamePrimaryUser]; return user; } - (IRCUser *)findUserOrCreate:(NSString *)nickname { NSParameterAssert(nickname != nil); IRCUser *user = [self findUser:nickname]; if (user == nil) { user = [[IRCUser alloc] initWithNickname:nickname onClient:self]; [self addUser:user]; } return user; } - (void)removeUser:(IRCUser *)user { NSParameterAssert(user != nil); [user cancelRemoveUserTimer]; NSString *hostmask = user.hostmask; if (hostmask) { [self clearAddressBookCacheForHostmask:hostmask]; } [self removeUserWithNickname:user.nickname]; } - (void)removeUserWithNickname:(NSString *)nickname { NSParameterAssert(nickname != nil); nickname = nickname.lowercaseString; @synchronized (self.userListPrivate) { [self.userListPrivate removeObjectForKey:nickname]; } } - (void)renameUser:(IRCUser *)user to:(NSString *)toNickname { NSParameterAssert(user != nil); NSParameterAssert(toNickname != nil); [self modifyUser:user withBlock:^(IRCUserMutable *userMutable) { userMutable.nickname = toNickname; }]; } - (void)renameUserWithNickname:(NSString *)fromNickname to:(NSString *)toNickname { NSParameterAssert(fromNickname != nil); NSParameterAssert(toNickname != nil); IRCUser *user = [self findUser:fromNickname]; if (user == nil) { return; } [self renameUser:user to:toNickname]; } - (void)modifyUser:(IRCUser *)user withBlock:(void(NS_NOESCAPE ^)(IRCUserMutable *userMutable))block { NSParameterAssert(user != nil); NSParameterAssert(block != nil); IRCUserMutable *userMutable = [user mutableCopy]; block(userMutable); if ([user.nickname isEqualToString:userMutable.nickname] == NO) { [self removeUser:user]; } [self addUser:userMutable]; } - (void)modifyUserUserWithNickname:(NSString *)nickname withBlock:(void(NS_NOESCAPE ^)(IRCUserMutable *userMutable))block { NSParameterAssert(nickname != nil); NSParameterAssert(block != nil); IRCUser *user = [self findUser:nickname]; if (user == nil) { return; } [self modifyUser:user withBlock:block]; } - (void)modifyUserWithNickname:(NSString *)nickname asAway:(BOOL)away { NSParameterAssert(nickname != nil); IRCUser *user = [self findUser:nickname]; if (user == nil) { return; } [self modifyUser:user asAway:away]; } - (void)modifyUser:(IRCUser *)user asAway:(BOOL)away { NSParameterAssert(user != nil); if (self.monitorAwayStatus == NO) { return; } if (away) { [user markAsAway]; } else { [user markAsReturned]; } [mainWindow() updateDrawingForUserInUserList:user]; } - (void)resetAwayStatusForUsers { [self.userList makeObjectsPerformSelector:@selector(markAsReturned)]; } #pragma mark - #pragma mark Send Raw Data - (void)sendLine:(NSString *)string { NSParameterAssert(string != nil); if (self.isConnected == NO) { [self printDebugInformationToConsole:TXTLS(@"IRC[6rj-2r]")]; return; } [self.socket sendLine:string]; worldController().bandwidthOut += string.length; worldController().messagesSent += 1; } - (void)send:(NSString *)string arguments:(NSArray *)arguments { NSParameterAssert(string != nil); NSParameterAssert(arguments != nil); NSString *stringToSend = [IRCSendingMessage stringWithCommand:string arguments:arguments]; [self sendLine:stringToSend]; } - (void)send:(NSString *)string, ... { NSParameterAssert(string != nil); NSMutableArray *argumentsOut = [NSMutableArray array]; va_list argumentsIn; va_start(argumentsIn, string); NSString *argumentInString = nil; while ((argumentInString = va_arg(argumentsIn, NSString *))) { [argumentsOut addObject:argumentInString]; } va_end(argumentsIn); [self send:string arguments:argumentsOut]; } #pragma mark - #pragma mark Sending Text - (void)inputText:(id)string asCommand:(IRCRemoteCommand)command { IRCTreeItem *destination = mainWindow().selectedItem; [self inputText:string asCommand:command destination:destination]; } - (void)inputText:(id)string destination:(IRCTreeItem *)destination { [self inputText:string asCommand:IRCRemoteCommandPrivmsg destination:destination]; } - (void)inputText:(id)string asCommand:(IRCRemoteCommand)command destination:(IRCTreeItem *)destination { NSParameterAssert(string != nil); NSParameterAssert(destination != nil); if (self.isTerminating) { return; } BOOL inputIsNSString = [string isKindOfClass:[NSString class]]; if (inputIsNSString == NO && [string isKindOfClass:[NSAttributedString class]] == NO) { NSAssert(NO, @"'string' must be NSString or NSAttributedString"); } if (command != IRCRemoteCommandPrivmsg && command != IRCRemoteCommandPrivmsgAction && command != IRCRemoteCommandNotice) { NSAssert(NO, @"Bad 'command' value"); } if ([string length] == 0) { return; } NSAttributedString *stringIn = nil; if (inputIsNSString) { stringIn = [NSAttributedString attributedStringWithString:string]; } else { stringIn = string; } NSArray *lines = ((NSAttributedString *)stringIn).splitIntoLines; /* Warn if the split value is above 4 lines or if the total string length exceeds TXMaximumIRCBodyLength times 4. */ if (lines.count > 4 || (stringIn.length > (TXMaximumIRCBodyLength * 4))) { BOOL continueInput = [TDCAlert modalAlertWithMessage:TXTLS(@"IRC[lql-8i]") title:TXTLS(@"IRC[u4c-7i]") defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]") suppressionKey:@"input_text_possible_flood_warning" suppressionText:nil]; if (continueInput == NO) { return; } } for (__strong NSAttributedString *line in lines) { NSString *lineString = line.string; BOOL isPrefixed = [lineString hasPrefix:@"/"]; if (destination.isClient) { if (isPrefixed) { line = [line attributedSubstringFromIndex:1]; } [self sendCommand:line]; continue; } NSUInteger lineLength = line.length; IRCChannel *channel = (IRCChannel *)destination; if (isPrefixed && [lineString hasPrefix:@"//"] == NO && lineLength > 1) { line = [line attributedSubstringFromIndex:1]; [self sendCommand:line]; } else { if (isPrefixed && lineLength > 1) { line = [line attributedSubstringFromIndex:1]; } [self sendText:line asCommand:command toChannel:channel]; } } } - (void)sendText:(NSAttributedString *)string asCommand:(IRCRemoteCommand)command toChannel:(IRCChannel *)channel { [self sendText:string asCommand:command toChannel:channel withEncryption:YES]; } - (void)sendText:(NSAttributedString *)string asCommand:(IRCRemoteCommand)command toChannel:(IRCChannel *)channel withEncryption:(BOOL)encryptText { NSParameterAssert(string != nil); NSParameterAssert(channel != nil); if (string.length == 0) { return; } if (channel.isUtility) { [self printDebugInformation:TXTLS(@"IRC[z2r-sd]") inChannel:channel]; return; } NSString *commandToSend = nil; TVCLogLineType lineType = TVCLogLineTypeUndefined; if (command == IRCRemoteCommandPrivmsg) { commandToSend = @"PRIVMSG"; lineType = TVCLogLineTypePrivateMessage; } else if (command == IRCRemoteCommandPrivmsgAction) { commandToSend = @"PRIVMSG"; lineType = TVCLogLineTypeAction; } else if (command == IRCRemoteCommandNotice) { commandToSend = @"NOTICE"; lineType = TVCLogLineTypeNotice; } NSParameterAssert(lineType != TVCLogLineTypeUndefined); NSArray *lines = string.splitIntoLines; for (NSAttributedString *line in lines) { NSMutableAttributedString *lineMutable = [line mutableCopy]; while (lineMutable.length > 0) { NSString *unencryptedMessage = [lineMutable stringFormattedForChannel:channel.name onClient:self withLineType:lineType]; TLOEncryptionManagerEncodingDecodingCallbackBlock encryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) { if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage] && wasEncrypted == NO) { return; } [self print:originalString by:self.userNickname inChannel:channel asType:lineType command:commandToSend receivedAt:[NSDate date] isEncrypted:wasEncrypted]; }; TLOEncryptionManagerInjectCallbackBlock injectionBlock = ^(NSString *encodedString) { NSString *sendMessage = encodedString; if (lineType == TVCLogLineTypeAction) { sendMessage = [NSString stringWithFormat:@"%cACTION %@%c", 0x01, sendMessage, 0x01]; } [self send:commandToSend, channel.name, sendMessage, nil]; }; if (encryptText == NO) { encryptionBlock(unencryptedMessage, NO); injectionBlock(unencryptedMessage); continue; } [self encryptMessage:unencryptedMessage directedAt:channel.name encodingCallback:encryptionBlock injectionCallback:injectionBlock]; } } [self processBundlesUserMessage:string.string command:commandToSend]; } - (void)sendPrivmsg:(NSString *)message toChannel:(IRCChannel *)channel { XRPerformBlockSynchronouslyOnMainQueue(^{ [self sendText:[NSAttributedString attributedStringWithString:message] asCommand:IRCRemoteCommandPrivmsg toChannel:channel]; }); } - (void)sendAction:(NSString *)message toChannel:(IRCChannel *)channel { XRPerformBlockSynchronouslyOnMainQueue(^{ [self sendText:[NSAttributedString attributedStringWithString:message] asCommand:IRCRemoteCommandPrivmsgAction toChannel:channel]; }); } - (void)sendNotice:(NSString *)message toChannel:(IRCChannel *)channel { XRPerformBlockSynchronouslyOnMainQueue(^{ [self sendText:[NSAttributedString attributedStringWithString:message] asCommand:IRCRemoteCommandNotice toChannel:channel]; }); } - (void)sendPrivmsgToSelectedChannel:(NSString *)message { IRCChannel *channel = [mainWindow() selectedChannelOn:self]; if (channel == nil) { return; } [self sendPrivmsg:message toChannel:channel]; } - (void)sendCTCPQuery:(NSString *)nickname command:(NSString *)command text:(nullable NSString *)text { NSParameterAssert(nickname != nil); NSParameterAssert(command != nil); NSString *stringToSend = nil; if (text == nil) { stringToSend = command; } else { stringToSend = [NSString stringWithFormat:@"%@ %@", command, text]; } NSString *message = [NSString stringWithFormat:@"%c%@%c", 0x01, stringToSend, 0x01]; [self send:@"PRIVMSG", nickname, message, nil]; } - (void)sendCTCPReply:(NSString *)nickname command:(NSString *)command text:(nullable NSString *)text { NSParameterAssert(nickname != nil); NSParameterAssert(command != nil); NSString *stringToSend = nil; if (text == nil) { stringToSend = command; } else { stringToSend = [NSString stringWithFormat:@"%@ %@", command, text]; } NSString *message = [NSString stringWithFormat:@"%c%@%c", 0x01, stringToSend, 0x01]; [self send:@"NOTICE", nickname, message, nil]; } - (void)sendCTCPPing:(NSString *)nickname { NSParameterAssert(nickname != nil); NSString *text = [NSString stringWithFormat:@"%f", [NSDate timeIntervalSince1970]]; [self sendCTCPQuery:nickname command:@"PING" text:text]; } #pragma mark - #pragma mark Send Command - (void)sendCommand:(id)string { [self sendCommand:string completeTarget:YES target:nil]; } - (void)sendCommand:(id)string completeTarget:(BOOL)completeTarget target:(nullable NSString *)targetChannelName { NSParameterAssert(string != nil); BOOL inputIsNSString = [string isKindOfClass:[NSString class]]; if (inputIsNSString == NO && [string isKindOfClass:[NSAttributedString class]] == NO) { NSAssert(NO, @"'string' must be NSString or NSAttributedString"); } if ([string length] == 0) { return; } NSMutableAttributedString *stringIn = nil; if (inputIsNSString) { stringIn = [[NSMutableAttributedString alloc] initWithString:string]; } else { stringIn = [string mutableCopy]; } if ([stringIn.string hasPrefix:@"/"]) { [stringIn deleteCharactersInRange:NSMakeRange(0, 1)]; } NSString *command = stringIn.tokenAsString; NSString *lowercaseCommand = command.lowercaseString; NSString *uppercaseCommand = command.uppercaseString; IRCClient *selectedClient = mainWindow().selectedClient; IRCChannel *selectedChannel = mainWindow().selectedChannel; IRCChannel *targetChannel = nil; NSInteger commandNumeric = [IRCCommandIndex indexOfLocalCommand:command]; if (completeTarget && targetChannelName != nil) { targetChannel = [self findChannel:targetChannelName]; } else if (completeTarget && selectedClient == self && selectedChannel) { targetChannel = selectedChannel; } switch (commandNumeric) { case IRCLocalCommandAme: // Command: AME case IRCLocalCommandAmsg: // Command: AMSG { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } IRCRemoteCommand sendAsCommand = 0; if (commandNumeric == IRCLocalCommandAmsg) { sendAsCommand = IRCRemoteCommandPrivmsg; } else { sendAsCommand = IRCRemoteCommandPrivmsgAction; } for (IRCClient *client in worldController().clientList) { if (client != self && [TPCPreferences amsgAllConnections] == NO) { continue; } for (IRCChannel *channel in client.channelList) { if (channel.isActive == NO || channel.isChannel == NO) { continue; } [client sendText:stringIn asCommand:sendAsCommand toChannel:channel]; } } break; } case IRCLocalCommandAquote: // Command: AQUOTE case IRCLocalCommandAraw: // Command: ARAW { NSAssertReturnLoopBreak(self.isConnected); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } for (IRCClient *client in worldController().clientList) { [client sendLine:stringIn.string]; } break; } case IRCLocalCommandAutojoin: // Command: AUTOJOIN { NSAssertReturnLoopBreak(self.isLoggedIn); [self performAutoJoinInitiatedByUser:YES]; break; } case IRCLocalCommandAway: // Command: AWAY { NSAssertReturnLoopBreak(self.isLoggedIn); for (IRCClient *client in worldController().clientList) { if (client != self && [TPCPreferences awayAllConnections] == NO) { continue; } NSString *comment = stringIn.string; /* We enforce the maximum away length here instead of -toggleAwayStatusWithComment: so that we have an easier way to know when it is truncated so the user can be informed. */ NSUInteger commentMaximumLength = client.supportInfo.maximumAwayLength; if (commentMaximumLength > 0 && comment.length > commentMaximumLength) { [client printDebugInformation:TXTLS(@"IRC[41y-p2]", self.networkNameAlt, commentMaximumLength)]; } [client toggleAwayStatusWithComment:comment]; } break; } case IRCLocalCommandBack: // Command: BACK { NSAssertReturnLoopBreak(self.isLoggedIn); for (IRCClient *client in worldController().clientList) { if (client != self && [TPCPreferences awayAllConnections] == NO) { continue; } [client toggleAwayStatus:NO withComment:nil]; } break; } case IRCLocalCommandCap: // Command: CAP case IRCLocalCommandCaps: // Command: CAPS { NSString *capabilities = self.enabledCapabilitiesStringValue; if (capabilities.length == 0) { [self printDebugInformation:TXTLS(@"IRC[5wa-lb]")]; } else { [self printDebugInformation:TXTLS(@"IRC[7p9-rs]", capabilities)]; } break; } case IRCLocalCommandClear: // Command: CLEAR { if (targetChannel) { [mainWindow() clearContentsOfChannel:targetChannel]; } else { [mainWindow() clearContentsOfClient:self]; } break; } case IRCLocalCommandClearall: // Command: CLEARALL { for (IRCClient *client in worldController().clientList) { if (client != self && [TPCPreferences clearAllConnections] == NO) { continue; } [mainWindow() clearContentsOfClient:client]; for (IRCChannel *channel in client.channelList) { [mainWindow() clearContentsOfChannel:channel]; } } break; } case IRCLocalCommandClose: // Command: CLOSE case IRCLocalCommandRemove: // Command: REMOVE { NSString *channelName = stringIn.tokenAsString; if (channelName.length == 0) { if (targetChannel) { [worldController() destroyChannel:targetChannel]; } break; } IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { [self printDebugInformation:TXTLS(@"IRC[pxa-ox]", channelName)]; break; } [worldController() destroyChannel:channel]; break; } case IRCLocalCommandConn: // Command: CONN { NSString *serverAddress = stringIn.lowercaseGetToken; if (serverAddress.length > 0) { if (serverAddress.isValidInternetAddress == NO) { [self printDebugInformation:TXTLS(@"IRC[zef-q9]")]; break; } self.temporaryServerAddressOverride = serverAddress; } if (self.isConnecting || self.isConnected) { __weak IRCClient *weakSelf = self; self.disconnectCallback = ^{ [weakSelf connect]; }; [self quit]; break; } [self connect]; break; } case IRCLocalCommandCtcp: // Command: CTCP case IRCLocalCommandCtcpreply: // Command: CTCPREPLY { NSAssertReturnLoopBreak(self.isLoggedIn); if (targetChannel && targetChannel != selectedChannel) { if (targetChannel.isUtility) { [self printDebugInformation:TXTLS(@"sxf-qx")]; break; } targetChannelName = targetChannel.name; } else { targetChannelName = stringIn.tokenAsString; } NSString *subCommand = stringIn.uppercaseGetToken; if (subCommand.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } if (commandNumeric == IRCLocalCommandCtcp) { if ([subCommand isEqualToString:@"PING"]) { [self sendCTCPPing:targetChannelName]; } else { [self sendCTCPQuery:targetChannelName command:subCommand text:stringIn.string]; } } else { [self sendCTCPReply:targetChannelName command:subCommand text:stringIn.string]; } break; } case IRCLocalCommandCycle: // Command: CYCLE case IRCLocalCommandHop: // Command: HOP case IRCLocalCommandRejoin: // Command: REJOIN { NSAssertReturnLoopBreak(self.isLoggedIn); if (targetChannel == nil || targetChannel.isChannel == NO) { [self printDebugInformation:TXTLS(@"IRC[g01-qn]")]; break; } [self partChannel:targetChannel]; [self forceJoinChannel:targetChannel.name password:targetChannel.secretKey]; break; } case IRCLocalCommandDehalfop: // Command: DEHALFOP case IRCLocalCommandDeop: // Command: DEOP case IRCLocalCommandDevoice: // Command: DEVOICE case IRCLocalCommandHalfop: // Command: HALFOP case IRCLocalCommandOp: // Command: OP case IRCLocalCommandVoice: // Command: VOICE { NSAssertReturnLoopBreak(self.isLoggedIn); BOOL modeIsSet = (commandNumeric == IRCLocalCommandOp || commandNumeric == IRCLocalCommandHalfop || commandNumeric == IRCLocalCommandVoice); NSString *modeSymbol = nil; if (commandNumeric == IRCLocalCommandOp || commandNumeric == IRCLocalCommandDeop) { modeSymbol = @"o"; } else if (commandNumeric == IRCLocalCommandHalfop || commandNumeric == IRCLocalCommandDehalfop) { modeSymbol = @"h"; } else if (commandNumeric == IRCLocalCommandVoice || commandNumeric == IRCLocalCommandDevoice) { modeSymbol = @"v"; } if ([self.supportInfo modeSymbolIsUserPrefix:modeSymbol] == NO) { [self printDebugInformation:TXTLS(@"IRC[dwi-d1]", modeSymbol)]; break; } if ([self stringIsChannelName:stringIn.string] == NO) { if (targetChannel && targetChannel.isChannel) { targetChannelName = targetChannel.name; } else { [self printDebugInformation:TXTLS(@"IRC[g01-qn]")]; break; } } else { targetChannelName = stringIn.tokenAsString; } NSString *nicknamesString = stringIn.string; if (nicknamesString.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } NSArray *modeChanges = [self compileListOfModeChangesForModeSymbol:modeSymbol modeIsSet:modeIsSet parameterString:nicknamesString]; for (NSString *modeChange in modeChanges) { [self send:@"MODE", targetChannelName, modeChange, nil]; } break; } case IRCLocalCommandDebug: // Command: DEBUG case IRCLocalCommandEcho: // Command: ECHO { if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } NSString *stringInString = stringIn.string; if ([stringInString isEqualToStringIgnoringCase:@"raw on"]) { [self createRawDataLogQuery]; } else if ([stringInString isEqualToStringIgnoringCase:@"raw off"]) { [self destroyRawDataLogQuery]; } else { [self printDebugInformation:stringInString]; } break; } case IRCLocalCommandDefaults: // Command: DEFAULTS { if (stringIn.length == 0) { [self printDebugInformation:TXTLS(@"IRC[1dz-jb]")]; break; } NSString *action = stringIn.tokenAsString; /* Present help */ if ([action isEqualToString:@"help"]) { NSString *help = TXTLS(@"IRC[bkk-lo]"); [self printDebugInformationMultiline:help]; break; } /* Present list of features */ else if ([action isEqualToString:@"features"]) { [TLOpenLink openWithString:@"https://help.codeux.com/textual/Command-Reference.kb#cr=defaults" inBackground:NO]; break; } /* Prepare to toggle feature */ NSString *feature = stringIn.tokenInsideQuotes.string; BOOL applyToAll = [feature isEqualToString:@"-a"]; if (applyToAll) { feature = stringIn.tokenInsideQuotes.string; } NSDictionary *features = @{ @"Ignore Notifications by Private ZNC Users" : @"setZncIgnoreUserNotifications:", @"Send Authentication Requests to UserServ" : @"setSendAuthenticationRequestsToUserServ:", @"Disable Automatic SASL EXTERNAL Response" : @"setSaslAuthenticationDisableExternalMechanism:", @"Send WHO Command Requests to Channels" : @"setSendWhoCommandRequestsToChannels:", }; BOOL enableFeature = [action isEqualToString:@"enable"]; /* Cannot toggle feature if the user doesn't tell us which */ if (feature.length == 0) { [self printDebugInformation:TXTLS(@"IRC[1dz-jb]")]; break; } /* Make sure the feature exists */ if ([features containsKey:feature] == NO) { if (enableFeature) { [self printDebugInformation:TXTLS(@"IRC[pc4-67]", feature)]; } else { [self printDebugInformation:TXTLS(@"IRC[d7y-pv]", feature)]; } break; } /* Toggle the feature by mutating the client's configuration, invoking the appropriate method, then saving it. */ void (^toggleFeature)(IRCClient *, NSString *, BOOL) = ^(IRCClient *client, NSString *featureKey, BOOL featureValue) { NSString *selectorString = features[featureKey]; SEL selector = NSSelectorFromString(selectorString); IRCClientConfigMutable *mutableClientConfig = [client.config mutableCopy]; NSMethodSignature *signature = [mutableClientConfig methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:mutableClientConfig]; [invocation setSelector:selector]; [invocation setArgument:&featureValue atIndex:2]; [invocation invoke]; client.config = mutableClientConfig; }; /* Toggle feature */ for (IRCClient *client in worldController().clientList) { if (client != self && applyToAll == NO) { continue; } toggleFeature(client, feature, enableFeature); if (enableFeature) { [client printDebugInformation:TXTLS(@"IRC[5ke-18]", feature)]; } else { [client printDebugInformation:TXTLS(@"IRC[0gn-cb]", feature)]; } } /* Save modified client */ [worldController() save]; break; } case IRCLocalCommandGetscripts: // Command: GETSCRIPTS { [sharedPluginManager() extrasInstallerLaunchInstaller]; break; } case IRCLocalCommandGline: // Command: GLINE case IRCLocalCommandGzline: // Command: GZLINE case IRCLocalCommandShun: // Command: SHUN case IRCLocalCommandTempshun: // Command: TEMPSHUN case IRCLocalCommandZline: // Command: ZLINE { NSAssertReturnLoopBreak(self.isLoggedIn); NSString *segment1 = stringIn.getTokenAsString; NSString *segment2 = stringIn.getTokenAsString; [self send:uppercaseCommand, segment1, segment2, stringIn.string, nil]; break; } case IRCLocalCommandGoto: // Command: GOTO { if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } NSString *needle = stringIn.tokenAsString; IRCTreeItem *bestMatch = mainWindow().selectedItem; CGFloat bestScore = 0.0; for (IRCClient *client in worldController().clientList) { for (IRCChannel *channel in client.channelList) { CGFloat currentScore = [channel.name compareWithWord:needle lengthPenaltyWeight:0.1]; if (currentScore > bestScore) { bestMatch = channel; bestScore = currentScore; } } } [mainWindow() select:bestMatch]; break; } case IRCLocalCommandIgnore: // Command: IGNORE case IRCLocalCommandUnignore: // Command: UNIGNORE { BOOL isIgnoreCommand = (commandNumeric == IRCLocalCommandIgnore); if (stringIn.length == 0 || targetChannel == nil) { if (isIgnoreCommand) { [menuController() showServerPropertiesSheetForClient:self withSelection:TDCServerPropertiesSheetSelectionNewIgnoreEntry context:@""]; } else { [menuController() showServerPropertiesSheetForClient:self withSelection:TDCServerPropertiesSheetSelectionAddressBook context:nil]; } break; } NSString *nickname = stringIn.tokenAsString; IRCUser *member = [self findUser:nickname]; if (member == nil) { if (isIgnoreCommand) { [menuController() showServerPropertiesSheetForClient:self withSelection:TDCServerPropertiesSheetSelectionNewIgnoreEntry context:nickname]; } else { [menuController() showServerPropertiesSheetForClient:self withSelection:TDCServerPropertiesSheetSelectionAddressBook context:nil]; } break; } /* Build list of ignores that already match the user's host */ NSString *hostmask = member.hostmask; if (hostmask == nil) { hostmask = [NSString stringWithFormat:@"%@!*@*", nickname]; } NSMutableArray *matchedIgnores = [NSMutableArray array]; for (IRCAddressBookEntry *ignore in self.config.ignoreList) { if (ignore.entryType != IRCAddressBookEntryTypeIgnore) { continue; } if ([ignore checkMatch:hostmask]) { [matchedIgnores addObject:ignore]; } } /* Cancel if there is nothing to change */ if (isIgnoreCommand) { if (matchedIgnores.count > 0) { [self printDebugInformation:TXTLS(@"IRC[5ix-zn]", member.nickname)]; break; } } else { if (matchedIgnores.count == 0) { [self printDebugInformation:TXTLS(@"IRC[wu0-jp]", member.nickname)]; break; } else if (matchedIgnores.count > 1) { [self printDebugInformation:TXTLS(@"IRC[vrx-1f]", member.nickname)]; break; } } /* Modify ignore list and inform user of change */ NSMutableArray *mutableIgnoreList = [self.config.ignoreList mutableCopy]; if (isIgnoreCommand) { IRCAddressBookEntry *ignore = [IRCAddressBookEntry newIgnoreEntryForHostmask:member.banMask]; [self printDebugInformation:TXTLS(@"IRC[ret-20]", member.nickname, ignore.hostmask)]; [mutableIgnoreList addObject:ignore]; } else{ IRCAddressBookEntry *ignore = matchedIgnores[0]; [self printDebugInformation:TXTLS(@"IRC[jzg-g8]", member.nickname, ignore.hostmask)]; [mutableIgnoreList removeObjectIdenticalTo:ignore]; } /* Save modified ignore list */ IRCClientConfigMutable *mutableClientConfig = [self.config mutableCopy]; mutableClientConfig.ignoreList = mutableIgnoreList; self.config = mutableClientConfig; /* Clear cache */ /* If we have a host, then it's easy to clear only that. If we don't have a host, then we have to clear everything. */ if (hostmask) { [self clearAddressBookCacheForHostmask:hostmask]; } else { [self clearAddressBookCache]; } break; } case IRCLocalCommandInvite: // Command: INVITE { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } NSArray *nicknames = [stringIn.string componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([self stringIsChannelName:nicknames.lastObject] == NO) { if (targetChannel && targetChannel.isChannel) { targetChannelName = targetChannel.name; } else { [self printDebugInformation:TXTLS(@"IRC[g01-qn]")]; break; } } else { targetChannelName = nicknames.lastObject; nicknames = [nicknames subarrayWithRange:NSMakeRange(0, (nicknames.count - 1))]; } for (NSString *nickname in nicknames) { if ([self stringIsNickname:nickname] == NO) { continue; } [self send:@"INVITE", nickname, targetChannelName, nil]; } break; } case IRCLocalCommandIson: // Command: ISON { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [self createHiddenCommandResponses]; [self.requestedCommands recordIsonRequestOpenedAsVisible]; [self send:@"ISON", stringIn.string, nil]; break; } case IRCLocalCommandJ: // Command: J case IRCLocalCommandJoin: // Command: JOIN { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { if (targetChannel && targetChannel.isChannel) { targetChannelName = targetChannel.name; } else { [self printDebugInformation:TXTLS(@"IRC[g01-qn]")]; break; } } else { targetChannelName = stringIn.tokenAsString; if (targetChannelName.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } if ([self stringIsChannelNameOrZero:targetChannelName] == NO) { targetChannelName = [@"#" stringByAppendingString:targetChannelName]; } } [self joinUnlistedChannelsWithStringAndSelectBestMatch:targetChannelName passwords:stringIn.string]; break; } case IRCLocalCommandJoinRandom: // Command: JOIN_RANDOM { NSAssertReturnLoopBreak(self.isLoggedIn); NSInteger numberOfChannelsToJoin = 0; NSString *numberOfChannelsToken = stringIn.tokenAsString; if (numberOfChannelsToken.isNumericOnly) { numberOfChannelsToJoin = numberOfChannelsToken.integerValue; } if (numberOfChannelsToJoin <= 0) { numberOfChannelsToJoin = 1; } for (NSUInteger i = 0; i < numberOfChannelsToJoin; i++) { NSString *channelName = [NSString stringWithFormat:@"#debug-channel-%lu", TXRandomNumber(9999999)]; [self send:@"JOIN", channelName, nil]; } break; } case IRCLocalCommandBan: // Command: BAN case IRCLocalCommandKb: // Command: KB case IRCLocalCommandKick: // Command: KICK case IRCLocalCommandKickban: // Command: KICKBAN case IRCLocalCommandUnban: // Command: UNBAN case IRCLocalCommandUnquiet: // Command: UNQUIET case IRCLocalCommandQuiet: // Command: QUIET { NSAssertReturnLoopBreak(self.isLoggedIn); NSString *nickname = stringIn.tokenAsString; if ([self stringIsChannelName:nickname] == NO) { if (targetChannel && targetChannel.isChannel) { targetChannelName = targetChannel.name; } else { [self printDebugInformation:TXTLS(@"IRC[g01-qn]")]; break; } } else { targetChannelName = nickname; targetChannel = [self findChannel:targetChannelName]; nickname = stringIn.tokenAsString; } if (nickname.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } if (commandNumeric == IRCLocalCommandKickban || commandNumeric == IRCLocalCommandKb || commandNumeric == IRCLocalCommandBan || commandNumeric == IRCLocalCommandUnban || commandNumeric == IRCLocalCommandQuiet || commandNumeric == IRCLocalCommandUnquiet) { IRCChannelUser *member = [targetChannel findMember:nickname]; NSString *banMask = member.user.banMask; if (banMask == nil) { banMask = nickname; } NSString *modeSymbol = nil; if (commandNumeric == IRCLocalCommandQuiet || commandNumeric == IRCLocalCommandUnquiet) { modeSymbol = @"q"; } else { modeSymbol = @"b"; } if ([self.supportInfo modeSymbolIsUserPrefix:modeSymbol]) { [self printDebugInformation:TXTLS(@"IRC[dwi-d1]", modeSymbol)]; break; } if (commandNumeric == IRCLocalCommandUnban || commandNumeric == IRCLocalCommandUnquiet) { [self send:@"MODE", targetChannelName, [@"-" stringByAppendingString:modeSymbol], banMask, nil]; } else { [self send:@"MODE", targetChannelName, [@"+" stringByAppendingString:modeSymbol], banMask, nil]; } } if (commandNumeric == IRCLocalCommandKb || commandNumeric == IRCLocalCommandKick || commandNumeric == IRCLocalCommandKickban) { NSString *reason = stringIn.string; if (reason.length == 0) { reason = [TPCPreferences defaultKickMessage]; } NSUInteger reasonMaximumLength = self.supportInfo.maximumKickLength; if (reasonMaximumLength > 0 && reason.length > reasonMaximumLength) { [self printDebugInformation:TXTLS(@"IRC[59a-ir]", self.networkNameAlt, reasonMaximumLength)]; } [self send:@"KICK", targetChannelName, nickname, reason, nil]; } break; } case IRCLocalCommandKill: // Command: KILL { NSAssertReturnLoopBreak(self.isLoggedIn); NSString *nickname = stringIn.getTokenAsString; if (nickname.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } NSString *reason = stringIn.string; if (reason.length == 0) { reason = [TPCPreferences IRCopDefaultKillMessage]; } [self send:@"KILL", nickname, reason, nil]; break; } case IRCLocalCommandLagcheck: // Command: LAGCHECK case IRCLocalCommandMylag: // Command: MYLAG { NSAssertReturnLoopBreak(self.isLoggedIn); /* We only accept LAGCHECK CTCP responses from ourselves which means it is relatively safe to pack some data on the end of the CTCP so that we can provide ourselves some context. */ NSMutableDictionary *lagCheckContext = [NSMutableDictionary dictionaryWithCapacity:3]; lagCheckContext[@"connection"] = self.socket.uniqueIdentifier; lagCheckContext[@"time"] = @([NSDate timeIntervalSince1970]); if (commandNumeric == IRCLocalCommandMylag) { IRCChannel *selectedChannel = [mainWindow() selectedChannelOn:self]; if (selectedChannel) { lagCheckContext[@"channel"] = selectedChannel.name; } } NSString *ctcpContext = [lagCheckContext formDataUsingSeparator:@"&"]; [self sendCTCPQuery:self.userNickname command:@"LAGCHECK" text:ctcpContext]; [self printDebugInformation:TXTLS(@"IRC[qoh-kt]")]; break; } case IRCLocalCommandLeave: // Command: LEAVE case IRCLocalCommandPart: // Command: PART { NSAssertReturnLoopBreak(self.isLoggedIn); if ([self stringIsChannelName:stringIn.string] == NO) { if (targetChannel && targetChannel.isChannel) { targetChannelName = targetChannel.name; } else if (targetChannel) { [worldController() destroyChannel:targetChannel]; break; } else { break; } } else { targetChannelName = stringIn.tokenAsString; } NSString *reason = stringIn.string; if (reason.length == 0) { reason = self.config.normalLeavingComment; } [self send:@"PART", targetChannelName, reason, nil]; break; } case IRCLocalCommandList: // Command: LIST { NSAssertReturnLoopBreak(self.isLoggedIn); TDCServerChannelListDialog *channelListDialog = [self channelListDialog]; if (channelListDialog == nil) { [self createChannelListDialog]; } [self requestChannelList]; break; } case IRCLocalCommandM: // Command: M case IRCLocalCommandMode: // Command: MODE { NSAssertReturnLoopBreak(self.isLoggedIn); NSString *modeString = stringIn.string; if ( modeString.length == 0 || ([modeString hasPrefix:@"+"] || [modeString hasPrefix:@"-"])) { if (targetChannel && targetChannel.isChannel) { targetChannelName = targetChannel.name; } else { [self printInvalidSyntaxMessageForCommand:command]; break; } } else { targetChannelName = stringIn.tokenAsString; } if (modeString.length == 0) { [self send:@"MODE", targetChannelName, nil]; } else { [self send:@"MODE", targetChannelName, modeString, nil]; } break; } case IRCLocalCommandMute: // Command: MUTE { if ([TPCPreferences soundIsMuted]) { [self printDebugInformation:TXTLS(@"IRC[sdn-yr]")]; } else { [self printDebugInformation:TXTLS(@"IRC[u48-aa]")]; [menuController() toggleMuteOnNotificationSoundsShortcutOn:YES]; } break; } case IRCLocalCommandMyversion: // Command: MYVERSION { NSString *applicationName = [TPCApplicationInfo applicationNameWithoutVersion]; NSString *versionLong = [TPCApplicationInfo applicationVersion]; NSString *versionShort = [TPCApplicationInfo applicationVersionShort]; // NSString *buildScheme = [TPCApplicationInfo applicationBuildScheme]; NSString *downloadSource = @""; // Assume standalone by default NSString *buildType = @""; // Assume universal binary by default #if TEXTUAL_BUILT_AS_UNIVERSAL_BINARY == 0 NSString *hostType = nil; #if TARGET_CPU_ARM64 hostType = TXTLS(@"IRC[g1u-os]"); #elif TARGET_CPU_X86_64 hostType = TXTLS(@"IRC[swz-uj]"); #endif buildType = TXTLS(@"IRC[b8p-44]", hostType); #endif // Universal NSString *message = TXTLS(@"IRC[ccb-ur]", applicationName, versionShort, versionLong, downloadSource, buildType); if (targetChannel) { message = TXTLS(@"IRC[pqj-1y]", message); [self sendPrivmsg:message toChannel:targetChannel]; } else { [self printDebugInformationToConsole:message]; } break; } case IRCLocalCommandNick: // Command: NICK { /* Use -isConnected instead of -isLoggedIn so that user can change their nickname during registration phase before the 001 numeric is received. */ NSAssertReturnLoopBreak(self.isConnected); NSString *newNickname = stringIn.tokenAsString; if (newNickname.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } for (IRCClient *client in worldController().clientList) { if (client != self && [TPCPreferences nickAllConnections] == NO) { continue; } [client changeNickname:newNickname]; } break; } case IRCLocalCommandNotifybubble: // Command: NOTIFYBUBBLE { if ([self stringIsChannelName:stringIn.string]) { targetChannel = [self findChannel:stringIn.tokenAsString]; } else { targetChannel = nil; } if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } NSString *title = [TPCApplicationInfo applicationNameWithoutVersion]; NSString *message = stringIn.string; [sharedNotificationController() scheduleNotificationWithTitle:title message:message forChannel:targetChannel onClient:self]; break; } case IRCLocalCommandNotifysound: // Command: NOTIFYSOUND { NSString *soundName = stringIn.tokenAsString; if (soundName.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [TLOSoundPlayer playAlertSound:soundName]; break; } case IRCLocalCommandNotifyspeak: // Command: NOTIFYSPEAK { if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [[TXSharedApplication sharedSpeechSynthesizer] speak:stringIn.string]; break; } case IRCLocalCommandQuery: // Command: QUERY { NSString *nickname = stringIn.tokenAsString; if (nickname.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } if ([self stringIsNickname:nickname] == NO) { [self printDebugInformation:TXTLS(@"IRC[zef-q9]")]; break; } IRCChannel *query = [self findChannelOrCreate:nickname isPrivateMessage:YES]; [mainWindow() select:query]; if (stringIn.length > 0) { [self sendText:stringIn asCommand:IRCRemoteCommandPrivmsg toChannel:query]; } break; } case IRCLocalCommandQuote: // Command: QUOTE case IRCLocalCommandRaw: // Command: RAW { NSAssertReturnLoopBreak(self.isConnected); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [self sendLine:stringIn.string]; break; } case IRCLocalCommandQuit: // Command: QUIT { NSAssertReturnLoopBreak(self.isConnected); if (stringIn.length == 0) { [self quit]; } else { [self quitWithComment:stringIn.string]; } break; } case IRCLocalCommandNames: // Command: NAMES { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [self createHiddenCommandResponses]; [self send:@"NAMES", stringIn.string, nil]; break; } case IRCLocalCommandRecv: // Command: RECV { if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [self ircConnection:self.socket didReceiveData:stringIn.string]; break; } case IRCLocalCommandSetcolor: // Command: SETCOLOR { if ([TPCPreferences disableNicknameColorHashing]) { [self printDebugInformation:TXTLS(@"IRC[026-qv]")]; break; } NSString *nickname = stringIn.lowercaseGetToken; if (nickname.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } if ([self stringIsNickname:nickname] == NO) { [self printDebugInformation:TXTLS(@"IRC[8dy-6f]", nickname)]; break; } [menuController() memberChangeColor:nickname]; break; } case IRCLocalCommandSetqueryname: // Command: SETQUERYNAME { if (targetChannel == nil || targetChannel.isPrivateMessage == NO) { [self printDebugInformation:TXTLS(@"IRC[m6o-z1]")]; break; } NSString *nickname = stringIn.getTokenAsString; if (nickname.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } if ([self stringIsNickname:nickname] == NO) { [self printDebugInformation:TXTLS(@"IRC[zef-q9]")]; break; } IRCChannel *oldQuery = [self findChannel:nickname]; if (oldQuery) { BOOL deleteOldQuery = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[61s-jc]") title:TXTLS(@"Prompts[d22-76]", oldQuery.name) defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]")]; if (deleteOldQuery) { [worldController() destroyChannel:oldQuery]; } else { break; } } targetChannel.name = nickname; [mainWindow() reloadTreeItem:targetChannel]; [mainWindow() updateTitleFor:targetChannel]; break; } case IRCLocalCommandServer: // Command: SERVER { if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [IRCExtras createConnectionToServer:stringIn.string channelList:nil connectWhenCreated:YES]; break; } case IRCLocalCommandSslcontext: // Command: SSLCONTEXT { [self presentCertificateTrustInformation]; break; } case IRCLocalCommandTage: // Command: TAGE { NSTimeInterval timePassed = [NSDate timeIntervalSinceNow:[TPCApplicationInfo applicationBirthday]]; NSString *message = TXTLS(@"IRC[v9x-18]", TXHumanReadableTimeInterval(timePassed, NO, 0)); if (targetChannel) { [self sendPrivmsg:message toChannel:targetChannel]; } else { [self printDebugInformationToConsole:message]; } break; } case IRCLocalCommandTimer: // Command: TIMER { if (stringIn.length == 0) { [self printDebugInformation:TXTLS(@"IRC[jj9-94]")]; break; } NSString *action = stringIn.tokenAsString; /* Present help */ if ([action isEqualToString:@"help"]) { NSString *topic = stringIn.tokenAsString; [self printDebugInformation:TXTLS(@"IRC[aox-zz]")]; // divider if ([topic isEqualToStringIgnoringCase:@"add"]) { [self printDebugInformationMultiline:TXTLS(@"IRC[6r0-il]")]; } else if ([topic isEqualToStringIgnoringCase:@"remove"]) { [self printDebugInformationMultiline:TXTLS(@"IRC[i2d-x5]")]; } else if ([topic isEqualToStringIgnoringCase:@"list"]) { [self printDebugInformationMultiline:TXTLS(@"IRC[x1n-ve]")]; } else if ([topic isEqualToStringIgnoringCase:@"stop"]) { [self printDebugInformationMultiline:TXTLS(@"IRC[bx2-n1]")]; } else if ([topic isEqualToStringIgnoringCase:@"restart"]) { [self printDebugInformationMultiline:TXTLS(@"IRC[r27-tv]")]; } else { [self printDebugInformationMultiline:TXTLS(@"IRC[xkq-rt]")]; } break; } /* Stop timer */ if ([action isEqualToString:@"stop"]) { NSString *identifier = stringIn.tokenAsString; if (identifier.length == 0) { [self printDebugInformation:TXTLS(@"IRC[p6l-o4]")]; break; } IRCTimedCommand *timedCommand = [self timedCommandWithIdentifier:identifier]; if (timedCommand == nil) { [self printDebugInformation:TXTLS(@"IRC[vzu-xh]", identifier)]; break; } if (timedCommand.timerIsActive == NO) { [self printDebugInformation:TXTLS(@"IRC[ax6-n9]", identifier)]; break; } [self stopTimedCommand:timedCommand]; [self printDebugInformation:TXTLS(@"IRC[hs0-up]", identifier)]; break; } /* Restart timer */ if ([action isEqualToString:@"restart"]) { NSString *identifier = stringIn.tokenAsString; if (identifier.length == 0) { [self printDebugInformation:TXTLS(@"IRC[p6l-o4]")]; break; } IRCTimedCommand *timedCommand = [self timedCommandWithIdentifier:identifier]; if (timedCommand == nil) { [self printDebugInformation:TXTLS(@"IRC[vzu-xh]", identifier)]; break; } if ([self restartTimedCommand:timedCommand]) { [self printDebugInformation:TXTLS(@"IRC[qb7-mi]", identifier)]; } else { [self printDebugInformation:TXTLS(@"IRC[dgp-d4]", identifier)]; } break; } /* Remove timer */ if ([action isEqualToString:@"remove"]) { NSString *identifier = stringIn.tokenAsString; if (identifier.length == 0) { [self printDebugInformation:TXTLS(@"IRC[p6l-o4]")]; break; } if ([identifier isEqualToStringIgnoringCase:@"all"]) { [self removeTimedCommands]; [self printDebugInformation:TXTLS(@"IRC[808-bs]")]; break; } IRCTimedCommand *timedCommand = [self timedCommandWithIdentifier:identifier]; if (timedCommand == nil) { [self printDebugInformation:TXTLS(@"IRC[vzu-xh]", identifier)]; break; } [self removeTimedCommand:timedCommand]; [self printDebugInformation:TXTLS(@"IRC[p7s-is]", identifier)]; break; } /* List timers */ if ([action isEqualToString:@"list"]) { NSArray *timedCommands = [self listOfTimedCommands]; NSUInteger numberOfTimers = timedCommands.count; if (numberOfTimers == 0) { [self printDebugInformation:TXTLS(@"IRC[pqk-5k]")]; break; } else if (numberOfTimers == 1) { [self printDebugInformation:TXTLS(@"IRC[6ts-oi]", numberOfTimers)]; } else { [self printDebugInformation:TXTLS(@"IRC[q1m-1e]", numberOfTimers)]; } for (IRCTimedCommand *timedCommand in timedCommands) { NSString *description = [self descriptionForTimedCommand:timedCommand]; [self printDebugInformation:description]; } break; } /* Add timer */ /* This command is designed to be backwards compatible. Old syntax: "/timer " New syntax: "/timer " We check the value of the second argument. If it's a number, then we treat it as the new syntax which means that number is the repeat count for the timer. */ /* Parse interval */ NSString *intervalString = action; if (intervalString.length == 0) { [self printDebugInformation:TXTLS(@"IRC[jj9-94]")]; break; } NSInteger interval = intervalString.integerValue; if (interval <= 0) { [self printDebugInformation:TXTLS(@"IRC[327-pv]")]; break; } /* Parse repeat count and/or command */ /* Repeat argument is treated as such: == 0 — repeat and never stop == 1 - perform timer once, do not repeat > 1 — repeat number of times */ NSString *repeatString = stringIn.tokenAsString; NSInteger repeat = NSNotFound; NSString *command = stringIn.trimmedString; if ([repeatString contentsIsOfType:CSStringTypeAnyNumber]) { /* Contents of second argument is a number, which means we want to treat it as the repeat count. */ repeat = repeatString.integerValue; } else { /* Contents of second argument is NOT a number, which means we need to merge that back with the remainder of the command value. */ if (command.length == 0) { command = repeatString; } else { command = [repeatString stringByAppendingFormat:@" %@", command]; } } /* Perform additional validation */ if (repeat < 0) { [self printDebugInformation:TXTLS(@"IRC[eud-kc]")]; break; } if (command.length == 0) { [self printDebugInformation:TXTLS(@"IRC[jj9-94]")]; break; } /* If we have no repeat value, then treat is one pass. */ if (repeat == NSNotFound) { repeat = 1; } /* Add timer */ IRCTimedCommand *timedCommand = nil; if (targetChannel == nil) { timedCommand = [[IRCTimedCommand alloc] initWithCommand:command onClient:self]; } else { timedCommand = [[IRCTimedCommand alloc] initWithCommand:command onClient:self inChannel:targetChannel]; } [self addTimedCommand:timedCommand]; [self startTimedCommand:timedCommand interval:interval onRepeat:(repeat != 1) iterations:repeat]; break; } case IRCLocalCommandT: // Command: T case IRCLocalCommandTopic: // Command: TOPIC { NSAssertReturnLoopBreak(self.isLoggedIn); if ([self stringIsChannelName:stringIn.string] == NO) { if (targetChannel && targetChannel.isChannel) { targetChannelName = targetChannel.name; } else { break; } } else { targetChannelName = stringIn.tokenAsString; } NSString *topic = stringIn.stringFormattedForIRC; NSUInteger topicLength = topic.length; if (topicLength == 0) { [self send:@"TOPIC", targetChannelName, nil]; return; } NSUInteger topicLengthMaximum = self.supportInfo.maximumTopicLength; if (topicLengthMaximum > 0 && topicLength > topicLengthMaximum) { [self printDebugInformation:TXTLS(@"IRC[1oo-3b]", self.networkNameAlt, topicLengthMaximum)]; } [self send:@"TOPIC", targetChannelName, topic, nil]; break; } case IRCLocalCommandUmode: // Command: UMODE { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { [self send:@"MODE", self.userNickname, nil]; } else { [self send:@"MODE", self.userNickname, stringIn.string, nil]; } break; } case IRCLocalCommandUnmute: // Command: UNMUTE { if ([TPCPreferences soundIsMuted] == NO) { [self printDebugInformation:TXTLS(@"IRC[5rf-mj]")]; } else { [self printDebugInformation:TXTLS(@"IRC[190-f2]")]; [menuController() toggleMuteOnNotificationSoundsShortcutOn:NO]; } break; } case IRCLocalCommandWallops: // Command: WALLOPS { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [self send:@"WALLOPS", stringIn.string, nil]; break; } case IRCLocalCommandMonitor: // Command: MONITOR case IRCLocalCommandWatch: // Command: WATCH { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } BOOL isClearCommand = NO; BOOL isModifierCommand = NO; /* UnrealIRCd splits arguments using space and loops through all of them which means a modifier or clear command can appear at the same time. */ NSArray *arguments = [stringIn.string componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *argument in arguments) { if ([argument hasPrefix:@"-"] || [stringIn.string hasPrefix:@"+"] ) { isModifierCommand = YES; break; } else if ([argument isEqualIgnoringCase:@"c"]) { isClearCommand = YES; } } if (isModifierCommand) { [self printDebugInformation:TXTLS(@"IRC[khw-4y]")]; break; } /* The clear command doesn't produce a result */ if (isClearCommand == NO) { [self createHiddenCommandResponses]; } [self sendCommand:uppercaseCommand withData:stringIn.string]; break; } case IRCLocalCommandWho: // Command: WHO { NSAssertReturnLoopBreak(self.isLoggedIn); if (stringIn.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } [self createHiddenCommandResponses]; [self.requestedCommands recordWhoRequestOpenedAsVisible]; [self send:@"WHO", stringIn.string, nil]; break; } case IRCLocalCommandWeights: // Command: WEIGHTS { if (targetChannel == nil || targetChannel.isChannel == NO) { [self printDebugInformation:TXTLS(@"IRC[g01-qn]")]; break; } [self printDebugInformation:TXTLS(@"IRC[zud-u3]", targetChannel.name)]; BOOL haveWeights = NO; for (IRCChannelUser *member in targetChannel.memberList) { double incomingWeight = member.incomingWeight; double outgoingWeight = member.outgoingWeight; CGFloat weight = (incomingWeight + outgoingWeight); if (weight > 0) { haveWeights = YES; [self printDebugInformation:TXTLS(@"IRC[24r-8c]", member.user.nickname, outgoingWeight, incomingWeight, weight)]; } } if (haveWeights == NO) { [self printDebugInformation:TXTLS(@"IRC[dje-41]")]; } break; } case IRCLocalCommandWhois: // Command: WHOIS { NSAssertReturnLoopBreak(self.isLoggedIn); NSString *nickname1 = stringIn.tokenAsString; if (nickname1.length == 0) { if (targetChannel && targetChannel.isPrivateMessage) { nickname1 = targetChannel.name; } else { [self printInvalidSyntaxMessageForCommand:command]; break; } } NSString *nickname2 = stringIn.tokenAsString; if (nickname2.length == 0) { [self send:@"WHOIS", nickname1, nickname1, nil]; } else { [self send:@"WHOIS", nickname1, nickname2, nil]; } break; } case IRCLocalCommandMe: // Command: ME case IRCLocalCommandMsg: // Command: MSG case IRCLocalCommandNotice: // Command: NOTICE case IRCLocalCommandOmsg: // Command: OMSG case IRCLocalCommandOnotice: // Command: ONOTICE case IRCLocalCommandSme: // Command: SME case IRCLocalCommandSmsg: // Command: SMSG case IRCLocalCommandUme: // Command: UME case IRCLocalCommandUmsg: // Command: UMSG case IRCLocalCommandUnotice: // Command: UNOTICE { /* Where would se send data to? */ if (self.isLoggedIn == NO) { [self printDebugInformationToConsole:TXTLS(@"IRC[6rj-2r]")]; break; } /* Establish context for comand */ NSString *channelNamePrefix = nil; BOOL isOperatorMessage = NO; BOOL isSecretMessage = NO; BOOL isUnencryptedMessage = NO; NSString *commandToSend = nil; TVCLogLineType lineType = TVCLogLineTypeUndefined; if (commandNumeric == IRCLocalCommandMsg || commandNumeric == IRCLocalCommandOmsg || commandNumeric == IRCLocalCommandSmsg || commandNumeric == IRCLocalCommandUmsg) { commandToSend = @"PRIVMSG"; lineType = TVCLogLineTypePrivateMessage; isOperatorMessage = (commandNumeric == IRCLocalCommandOmsg); isSecretMessage = (commandNumeric == IRCLocalCommandSmsg); isUnencryptedMessage = (commandNumeric == IRCLocalCommandUmsg); } else if (commandNumeric == IRCLocalCommandMe || commandNumeric == IRCLocalCommandSme || commandNumeric == IRCLocalCommandUme) { commandToSend = @"PRIVMSG"; lineType = TVCLogLineTypeAction; isSecretMessage = (commandNumeric == IRCLocalCommandSme); isUnencryptedMessage = (commandNumeric == IRCLocalCommandUme); } else if (commandNumeric == IRCLocalCommandNotice || // Command: NOTICE commandNumeric == IRCLocalCommandOnotice || // Command: ONOTICE commandNumeric == IRCLocalCommandUnotice) // Command: UNOTICE { commandToSend = @"NOTICE"; lineType = TVCLogLineTypeNotice; isOperatorMessage = (commandNumeric == IRCLocalCommandOnotice); isUnencryptedMessage = (commandNumeric == IRCLocalCommandUnotice); } if (isOperatorMessage) { channelNamePrefix = [self.supportInfo statusMessagePrefixForModeSymbol:@"o"]; /* If the user is sending an operator message and the user mode +o does not exist, then fail here. The user may be trying to send something secret with an expectation for privacy and we cannot deliver that. */ if (channelNamePrefix == nil) { [self printDebugInformation:TXTLS(@"IRC[54l-h7]", @"o")]; break; } } /* Pick the best target */ /* All actions except (SME) should use the target channel */ /* Operator messages should use the target channel unless the string in is a channel name */ /* All other scenarios use the string in (token) */ if (isSecretMessage == NO && lineType == TVCLogLineTypeAction && targetChannel) { if (targetChannel.isUtility) { [self printDebugInformation:TXTLS(@"sxf-qx")]; break; } targetChannelName = targetChannel.name; } else if (isOperatorMessage && [self stringIsChannelName:stringIn.string] == NO && targetChannel.isChannel) { targetChannelName = targetChannel.name; } else { targetChannelName = stringIn.tokenAsString; } if (targetChannelName.length == 0) { [self printInvalidSyntaxMessageForCommand:command]; break; } /* Actions are allowed to have an empty message but all other types are not. Empty actions use a whitespace. */ if (stringIn.length == 0) { if (lineType == TVCLogLineTypeAction) { [stringIn replaceCharactersInRange:NSMakeRange(0, 0) withString:@" "]; } else { break; } } /* At this point, the following will occur: 1. Each destination is looped over 1. Prefix characters are removed from the destination name 2. Try to find channel that already exists which matches the destination. If a channel does not exist, then we create one depending on whether this is a secret message. 3. The message is then encrypted and sent off. */ NSArray *destinations = [targetChannelName componentsSeparatedByString:@","]; IRCChannel *destinationToSelect = nil; for (__strong NSString *destinationName in destinations) { /* If the user prefixed the target with a mode (e.g. +#channel) to indicate that they want the message only seen by that group of users, then we first have to remove that prefix to perform our own processing of the target. When it comes time to send the message, then the prefix is added back to the target name. */ NSString *destinationNamePrefix = [self.supportInfo extractStatusMessagePrefixFromChannelNamed:destinationName]; if (destinationNamePrefix.length == 0) { destinationNamePrefix = channelNamePrefix; } else { destinationName = [destinationName substringFromIndex:1]; } /* Locate object that matches destination */ IRCChannel *destination = [self findChannel:destinationName]; if (isSecretMessage == NO) { /* If the destination does not exist and this isn't a secret message, then create a private message if the destination is believed to be a user. */ if (destination == nil && [self stringIsNickname:destinationName]) { destination = [worldController() createPrivateMessage:destinationName onClient:self]; } /* Define the channel that will be selected */ if ([TPCPreferences giveFocusOnMessageCommand]) { if (destinationToSelect == nil) { destinationToSelect = destination; } } } /* Add prefix back if the destination is a channel */ BOOL destinationIsChannel = (destination.isChannel || (destination == nil && [self stringIsChannelName:destinationName])); if (destinationNamePrefix && destinationIsChannel) { destinationName = [destinationNamePrefix stringByAppendingString:destinationName]; } /* Break text up into substrings which can then be sent. */ NSMutableAttributedString *lineMutable = [stringIn mutableCopy]; while (lineMutable.length > 0) { NSString *unencryptedMessage = [lineMutable stringFormattedForChannel:destinationName onClient:self withLineType:lineType]; TLOEncryptionManagerEncodingDecodingCallbackBlock encryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) { if (destination == nil) { return; } if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage] && wasEncrypted == NO) { return; } [self print:originalString by:self.userNickname inChannel:destination asType:lineType command:command receivedAt:[NSDate date] isEncrypted:wasEncrypted]; }; TLOEncryptionManagerInjectCallbackBlock injectionBlock = ^(NSString *encodedString) { NSString *sendMessage = encodedString; if (lineType == TVCLogLineTypeAction) { sendMessage = [NSString stringWithFormat:@"%cACTION %@%c", 0x01, sendMessage, 0x01]; } [self send:commandToSend, destinationName, sendMessage, nil]; }; if (destination == nil || isUnencryptedMessage) { encryptionBlock(unencryptedMessage, NO); injectionBlock(unencryptedMessage); continue; } [self encryptMessage:unencryptedMessage directedAt:destination.name encodingCallback:encryptionBlock injectionCallback:injectionBlock]; } } // destination for() /* Focus destination */ if (destinationToSelect) { [mainWindow() select:destinationToSelect]; } break; } default: { /* Find an addon responsible for this command. */ NSString *addonPath = nil; BOOL pluginFound = NO; BOOL scriptFound = NO; BOOL commandIsReserved = NO; [sharedPluginManager() findHandlerForOutgoingCommand:lowercaseCommand path:&addonPath isReserved:&commandIsReserved isScript:&scriptFound isExtension:&pluginFound]; if (commandIsReserved) { [sharedPluginManager() extrasInstallerAskUserIfTheyWantToInstallCommand:lowercaseCommand]; } /* Perform script or plugin. */ if (pluginFound && scriptFound) { [self printDebugInformation:TXTLS(@"IRC[d3c-9b]", uppercaseCommand)]; break; } else if (pluginFound && scriptFound == NO) { [self processBundlesUserMessage:stringIn.string command:lowercaseCommand]; break; } else if (pluginFound == NO && scriptFound) { NSMutableDictionary *context = [NSMutableDictionary dictionaryWithCapacity:3]; [context maybeSetObject:stringIn.string forKey:@"inputString"]; [context maybeSetObject:addonPath forKey:@"path"]; [context maybeSetObject:targetChannel.name forKey:@"targetChannel"]; [self executeTextualCmdScriptInContext:context]; break; } /* Send input to server */ [self sendCommand:uppercaseCommand withData:stringIn.string]; break; } } } - (void)sendCommand:(NSString *)command withData:(NSString *)data { NSParameterAssert(command != nil); NSParameterAssert(data != nil); NSString *stringToSend = [NSString stringWithFormat:@"%@ %@", command, data]; [self sendLine:stringToSend]; } - (void)printInvalidSyntaxMessageForCommand:(NSString *)command { NSParameterAssert(command != nil); NSString *syntax = [IRCCommandIndex syntaxForLocalCommand:command]; if (syntax == nil) { return; } [self printDebugInformation:TXTLS(@"IRC[atq-93]", syntax)]; } #pragma mark - #pragma mark Log File - (void)reopenLogFileIfNeeded { if ([TPCPreferences logToDiskIsEnabled]) { if ( self.logFile) { [self.logFile reopenIfNeeded]; } } else { [self closeLogFile]; } } - (void)closeLogFile { if (self.logFile == nil) { return; } [self.logFile close]; } - (void)writeToLogLineToLogFile:(TVCLogLine *)logLine { NSParameterAssert(logLine != nil); if ([TPCPreferences logToDiskIsEnabled] == NO) { return; } // Perform addition before if statement to avoid infinite loop self.logFileSessionCount += 1; if (self.logFileSessionCount == 1) { [self logFileWriteSessionBegin]; } if (self.logFile == nil) { self.logFile = [[TLOFileLogger alloc] initWithClient:self]; } [self.logFile writeLogLine:logLine]; } - (void)logFileRecordSessionChanged:(BOOL)toNewSession inChannel:(nullable IRCChannel *)channel { NSParameterAssert(channel.isUtility == NO); NSString *localization = nil; if (toNewSession) { localization = @"IRC[qrg-ua]"; } else { localization = @"IRC[d5d-uy]"; } TVCLogLineMutable *logLine = [TVCLogLineMutable new]; /* ============================ */ logLine.messageBody = @" "; if (channel) { [channel writeToLogLineToLogFile:logLine]; } else { [self writeToLogLineToLogFile:logLine]; } /* ============================ */ logLine.messageBody = TXTLS(localization); if (channel) { [channel writeToLogLineToLogFile:logLine]; } else { [self writeToLogLineToLogFile:logLine]; } /* ============================ */ logLine.messageBody = @" "; if (channel) { [channel writeToLogLineToLogFile:logLine]; } else { [self writeToLogLineToLogFile:logLine]; } } - (void)endLoggingSessions { for (IRCChannel *channel in self.channelList) { if (channel.isUtility) { continue; } [channel logFileWriteSessionEnd]; } [self logFileWriteSessionEnd]; } - (void)logFileWriteSessionBegin { [self logFileRecordSessionChanged:YES inChannel:nil]; } - (void)logFileWriteSessionEnd { [self logFileRecordSessionChanged:NO inChannel:nil]; self.logFileSessionCount = 0; } #pragma mark - #pragma mark Print - (NSString *)formatNickname:(NSString *)nickname inChannel:(nullable IRCChannel *)channel { return [self formatNickname:nickname inChannel:channel withFormat:nil]; } - (NSString *)formatNickname:(NSString *)nickname inChannel:(nullable IRCChannel *)channel withFormat:(nullable NSString *)format { NSParameterAssert(nickname != nil); if (format.length == 0) { format = themeSettings().themeNicknameFormat; } if (format.length == 0) { format = [TPCPreferences themeNicknameFormat]; } if (format.length == 0) { format = [TPCPreferences themeNicknameFormatDefault]; } NSString *modeSymbol = @""; if (channel.isChannel) { IRCChannelUser *member = [channel findMember:nickname]; if (member) { modeSymbol = member.mark; } } NSString *formatMarker = @"%"; NSString *chunk = nil; NSScanner *scanner = [NSScanner scannerWithString:format]; scanner.charactersToBeSkipped = nil; NSMutableString *buffer = [NSMutableString new]; while (scanner.atEnd == NO) { if ([scanner scanUpToString:formatMarker intoString:&chunk]) { [buffer appendString:chunk]; } if ([scanner scanString:formatMarker intoString:nil] == NO) { break; } NSInteger paddingWidth = 0; [scanner scanInteger:&paddingWidth]; /* Read the output type marker */ NSString *outputValue = nil; if ([scanner scanString:@"@" intoString:nil]) { outputValue = modeSymbol; } else if ([scanner scanString:@"n" intoString:nil]) { outputValue = nickname; } else if ([scanner scanString:formatMarker intoString:nil]) { outputValue = formatMarker; } if (outputValue) { if (paddingWidth < 0 && ABS(paddingWidth) > outputValue.length) { NSString *paddedString = [@"" stringByPaddingToLength:(ABS(paddingWidth) - outputValue.length) withString:@" " startingAtIndex:0]; [buffer appendString:paddedString]; } [buffer appendString:outputValue]; if (paddingWidth > 0 && paddingWidth > outputValue.length) { NSString *paddedString = [@"" stringByPaddingToLength:(paddingWidth - outputValue.length) withString:@" " startingAtIndex:0]; [buffer appendString:paddedString]; } } } return [buffer copy]; } - (void)printAndLog:(TVCLogLine *)logLine completionBlock:(TVCLogControllerPrintOperationCompletionBlock)completionBlock { NSParameterAssert(logLine != nil); [self.viewController print:logLine completionBlock:completionBlock]; [self writeToLogLineToLogFile:logLine]; } - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command { [self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:[NSDate date] isEncrypted:NO escapeMessage:YES referenceMessage:nil completionBlock:nil]; } - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command escapeMessage:(BOOL)escapeMessage { [self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:[NSDate date] isEncrypted:NO escapeMessage:escapeMessage referenceMessage:nil completionBlock:nil]; } - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command receivedAt:(NSDate *)receivedAt { [self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:receivedAt isEncrypted:NO escapeMessage:YES referenceMessage:nil completionBlock:nil]; } - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted { [self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:receivedAt isEncrypted:isEncrypted escapeMessage:YES referenceMessage:nil completionBlock:nil]; } - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted referenceMessage:(nullable IRCMessage *)referenceMessage { [self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:receivedAt isEncrypted:isEncrypted escapeMessage:YES referenceMessage:referenceMessage completionBlock:nil]; } - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted referenceMessage:(nullable IRCMessage *)referenceMessage completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock { [self print:messageBody by:nickname inChannel:channel asType:lineType command:command receivedAt:receivedAt isEncrypted:isEncrypted escapeMessage:YES referenceMessage:referenceMessage completionBlock:completionBlock]; } - (void)print:(NSString *)messageBody by:(nullable NSString *)nickname inChannel:(nullable IRCChannel *)channel asType:(TVCLogLineType)lineType command:(nullable NSString *)command receivedAt:(NSDate *)receivedAt isEncrypted:(BOOL)isEncrypted escapeMessage:(BOOL)escapeMessage referenceMessage:(nullable IRCMessage *)referenceMessage completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock { NSParameterAssert(messageBody != nil); NSParameterAssert(command != nil || referenceMessage != nil); if (self.isTerminating) { return; } /* If an operation does not specify a command value, then try to obtain it from the reference message. */ if (command == nil) { command = referenceMessage.command; } /* Prevent stupid plugin authors */ if ([channel isKindOfClass:[IRCChannel class]] == NO) { channel = nil; } /* Do not print this message? */ if (channel) { if ([self outputRuleMatchedInMessage:messageBody inChannel:channel]) { return; } } /* Define where the message originated */ NSString *localNickname = self.userNickname; TVCLogLineMemberType memberType = TVCLogLineMemberTypeNormal; if ([nickname isEqualToString:localNickname]) { memberType = TVCLogLineMemberTypeLocalUser; } /* Define list of highlight keywords */ BOOL matchHighlights = (channel && channel.config.ignoreHighlights == NO && (lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypeAction) && memberType == TVCLogLineMemberTypeNormal); NSMutableArray *excludeKeywords = nil; NSMutableArray *matchKeywords = nil; if (matchHighlights) { /* Global highlight keywords */ excludeKeywords = [[TPCPreferences highlightExcludeKeywords] mutableCopy]; matchKeywords = [[TPCPreferences highlightMatchKeywords] mutableCopy]; /* Self nickname keyword */ if ([TPCPreferences highlightMatchingMethod] != TXNicknameHighlightMatchTypeRegularExpression && [TPCPreferences highlightCurrentNickname]) { [matchKeywords addObjectWithoutDuplication:localNickname]; } /* Client/channel specific keywords */ NSArray *clientHighlightList = self.config.highlightList; NSString *channelId = channel.uniqueIdentifier; for (IRCHighlightMatchCondition *e in clientHighlightList) { NSString *matchChannelId = e.matchChannelId; if (matchChannelId.length > 0) { if ([matchChannelId isEqualToString:channelId] == NO) { continue; } } if (e.matchIsExcluded) { [excludeKeywords addObjectWithoutDuplication:e.matchKeyword]; } else { [matchKeywords addObjectWithoutDuplication:e.matchKeyword]; } } } // matchKeywords if (lineType == TVCLogLineTypeActionNoHighlight) { lineType = TVCLogLineTypeAction; } else if (lineType == TVCLogLineTypePrivateMessageNoHighlight) { lineType = TVCLogLineTypePrivateMessage; } /* Renderer attributes */ NSDictionary *rendererAttributes = nil; if (escapeMessage == NO) { rendererAttributes = @{ TVCLogRendererConfigurationDoNotEscapeBodyAttribute : @(YES) }; } /* Create new log entry */ TVCLogLineMutable *logLine = [TVCLogLineMutable new]; logLine.command = command.lowercaseString; logLine.lineType = lineType; logLine.memberType = memberType; logLine.isEncrypted = isEncrypted; logLine.excludeKeywords = excludeKeywords; logLine.highlightKeywords = matchKeywords; logLine.rendererAttributes = rendererAttributes; logLine.nickname = nickname; logLine.messageBody = messageBody; /* Has the date changed */ TVCLogLine *lastLine = ((channel) ? channel.lastLine : self.lastLine); if (lastLine == nil) { logLine.isFirstForDay = YES; } else { logLine.isFirstForDay = ([receivedAt isInSameDayAsDate:lastLine.receivedAt] == NO); } logLine.receivedAt = receivedAt; /* Print to server console if there is no channel */ if (channel == nil) { [self printAndLog:logLine completionBlock:completionBlock]; return; } /* Add scrollback marker to channel if conditions are met */ if ([TPCPreferences autoAddScrollbackMark]) { if ([mainWindow() isItemVisible:channel] == NO || mainWindow().mainWindow == NO) { if (channel.isUnread == NO && (lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeNotice)) { [channel.viewController mark]; } } } /* Print to channel */ [channel print:logLine completionBlock:completionBlock]; } - (void)printReply:(IRCMessage *)message { [self printReply:message inChannel:nil]; } - (void)printReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel { [self printReply:message inChannel:channel withSequence:1]; } - (void)printReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel withSequence:(NSUInteger)sequence { NSParameterAssert(message != nil); [self print:[message sequence:sequence] by:nil inChannel:channel asType:TVCLogLineTypeDebug command:message.command receivedAt:message.receivedAt]; } - (void)printUnknownReply:(IRCMessage *)message { [self printUnknownReply:message inChannel:nil]; } - (void)printUnknownReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel { [self printUnknownReply:message inChannel:channel withSequence:1]; } - (void)printUnknownReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel withSequence:(NSUInteger)sequence { NSParameterAssert(message != nil); [self print:[message sequence:sequence] by:nil inChannel:channel asType:TVCLogLineTypeDebug command:message.command receivedAt:message.receivedAt]; } - (void)printErrorReply:(IRCMessage *)message { [self printErrorReply:message inChannel:nil]; } - (void)printErrorReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel { [self printErrorReply:message inChannel:channel withSequence:NSNotFound]; } - (void)printErrorReply:(IRCMessage *)message inChannel:(nullable IRCChannel *)channel withSequence:(NSUInteger)sequence { NSParameterAssert(message != nil); NSString *sequenceMessage = nil; if (sequence == NSNotFound) { sequenceMessage = message.sequence; } else { sequenceMessage = [message sequence:sequence]; } NSString *errorMessage = TXTLS(@"IRC[3yo-gw]", message.commandNumeric, sequenceMessage); [self print:errorMessage by:nil inChannel:channel asType:TVCLogLineTypeDebug command:message.command]; } - (void)printError:(NSString *)errorMessage asCommand:(NSString *)command { [self print:errorMessage by:nil inChannel:nil asType:TVCLogLineTypeDebug command:command]; } - (void)printDebugInformationToConsole:(NSString *)message { [self printDebugInformationToConsole:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES]; } - (void)printDebugInformationToConsole:(NSString *)message asCommand:(NSString *)command { [self printDebugInformationToConsole:message asCommand:command escapeMessage:YES]; } - (void)printDebugInformationToConsole:(NSString *)message escapeMessage:(BOOL)escapeMessage { [self printDebugInformationToConsole:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage]; } - (void)printDebugInformationToConsole:(NSString *)message asCommand:(NSString *)command escapeMessage:(BOOL)escapeMessage { [self print:message by:nil inChannel:nil asType:TVCLogLineTypeDebug command:command escapeMessage:escapeMessage]; } - (void)printDebugInformation:(NSString *)message { [self printDebugInformation:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES]; } - (void)printDebugInformationMultiline:(NSString *)message { [message enumerateSplitOnNewLinesWithBlock:^(NSString *sequence, BOOL *stop) { [self printDebugInformation:sequence]; }]; } - (void)printDebugInformation:(NSString *)message asCommand:(NSString *)command { [self printDebugInformation:message asCommand:command escapeMessage:YES]; } - (void)printDebugInformation:(NSString *)message escapeMessage:(BOOL)escapeMessage { [self printDebugInformation:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage]; } - (void)printDebugInformation:(NSString *)message asCommand:(NSString *)command escapeMessage:(BOOL)escapeMessage { IRCChannel *channel = [mainWindow() selectedChannelOn:self]; [self printDebugInformation:message inChannel:channel asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES]; } - (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel { [self printDebugInformation:message inChannel:channel asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES]; } - (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel asCommand:(NSString *)command { [self print:message by:nil inChannel:channel asType:TVCLogLineTypeDebug command:command escapeMessage:YES]; } - (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel escapeMessage:(BOOL)escapeMessage { [self printDebugInformation:message inChannel:channel asCommand:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage]; } - (void)printDebugInformation:(NSString *)message inChannel:(IRCChannel *)channel asCommand:(NSString *)command escapeMessage:(BOOL)escapeMessage { [self print:message by:nil inChannel:channel asType:TVCLogLineTypeDebug command:command escapeMessage:escapeMessage]; } - (void)printDebugInformationInAllViews:(NSString *)message { [self printDebugInformationInAllViews:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:YES]; } - (void)printDebugInformationInAllViews:(NSString *)message asCommand:(NSString *)command { [self printDebugInformationInAllViews:message asCommand:command escapeMessage:YES]; } - (void)printDebugInformationInAllViews:(NSString *)message escapeMessage:(BOOL)escapeMessage { [self printDebugInformationInAllViews:message asCommand:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage]; } - (void)printDebugInformationInAllViews:(NSString *)message asCommand:(NSString *)command escapeMessage:(BOOL)escapeMessage { for (IRCChannel *channel in self.channelList) { [self printDebugInformation:message inChannel:channel asCommand:command escapeMessage:escapeMessage]; } [self printDebugInformationToConsole:message asCommand:command escapeMessage:escapeMessage]; } #pragma mark - #pragma mark IRCConnection Delegate - (void)resetAllPropertyValues { // Some properties are purposely excluded from this method // because their state must be kept or they are reset elsewhere [self.batchMessages dequeueEntries]; self.connectDelay = 0; self.invokingISONCommandForFirstTime = NO; self.isAutojoining = NO; self.isAutojoined = NO; self.autojoinDelayedWarningCount = 0; self.isConnected = NO; self.isConnecting = NO; self.isLoggedIn = NO; self.isQuitting = NO; self.isDisconnecting = NO; self.inWhoisResponse = NO; self.inWhowasResponse = NO; self.isWaitingForNickServ = NO; self.serverHasNickServ = NO; self.userIsIdentifiedWithNickServ = NO; self.userIsAway = NO; self.userIsIRCop = NO; self.isConnectedToZNC = NO; self.zncBouncerIsSendingCertificateInfo = NO; self.zncBouncerCertificateChainDataMutable = nil; self.zncBouncerIsPlayingBackHistory = NO; self.reconnectEnabled = NO; self.timeoutWarningShownToUser = NO; self.lastWhoRequestChannelListIndex = 0; self.server = nil; self.userHostmask = nil; self.userNickname = nil; self.tryingNicknameNumber = 0; self.tryingNicknameSentNickname = nil; self.preAwayUserNickname = nil; self.lastMessageReceived = 0; self.capabilities = 0; self.capabilityNegotiationIsPaused = NO; @synchronized (self.capabilitiesPending) { [self.capabilitiesPending removeAllObjects]; } @synchronized (self.userListPrivate) { [self.userListPrivate removeAllObjects]; } } - (void)changeStateOff { [self changeStateOffWithError:nil]; } - (void)changeStateOffWithError:(nullable NSError *)disconnectError { if (self.isConnecting == NO && self.isConnected == NO) { return; } BOOL isTerminating = self.isTerminating; self.socket = nil; [self removeTimedCommands]; [self removeRequestedCommands]; [self stopAutojoinTimer]; [self stopAutojoinNextJoinTimer]; [self stopAutojoinDelayedWarningTimer]; [self stopISONTimer]; [self stopPongTimer]; [self stopRetryTimer]; [self cancelPerformRequests]; if (isTerminating == NO && self.reconnectEnabled) { [self startReconnectTimer]; } [self.supportInfo reset]; [self clearAddressBookCache]; [self clearTrackedUsers]; if (isTerminating == NO) { /* -prepareForApplicationTermination in TVCLogController will cancel all operations for this client for us during termination. */ [[TXSharedApplication sharedPrintingQueue] cancelOperationsForClient:self]; IRCClientDisconnectMode disconnectType = self.disconnectType; if (disconnectError) { // TODO: Don't hardcode the error domain and code if ( disconnectError.code == IRCConnectionErrorCodeBadCertificate && [disconnectError.domain isEqualToString:IRCConnectionErrorDomain]) { disconnectType = IRCClientDisconnectModeBadCertificate; } [self printError:disconnectError.localizedDescription asCommand:TVCLogLineDefaultCommandValue]; } NSString *disconnectMessage = nil; switch (disconnectType) { case IRCClientDisconnectModeNormal: { disconnectMessage = TXTLS(@"IRC[9b4-10]"); break; } case IRCClientDisconnectModeComputerSleep: { disconnectMessage = TXTLS(@"IRC[drg-b7]"); break; } case IRCClientDisconnectModeBadCertificate: { disconnectMessage = TXTLS(@"IRC[zro-bg]"); break; } case IRCClientDisconnectModeServerRedirect: { disconnectMessage = TXTLS(@"IRC[wcl-po]"); break; } case IRCClientDisconnectModeReachabilityChange: { disconnectMessage = TXTLS(@"IRC[isx-fi]"); break; } } // switch() for (IRCChannel *channel in self.channelList) { if (channel.isActive == NO) { channel.errorOnLastJoinAttempt = NO; } else { [channel deactivate]; if (channel.isUtility) { continue; } [self printDebugInformation:disconnectMessage inChannel:channel]; } } [self printDebugInformationToConsole:disconnectMessage]; [self.viewController mark]; if (self.isConnected) { [self notifyEvent:TXNotificationTypeDisconnect lineType:TVCLogLineTypeDebug]; } [self postEventToViewController:@"serverDisconnected"]; } [self endLoggingSessions]; [self resetAllPropertyValues]; if (isTerminating == NO) { [mainWindow() reloadTreeGroup:self]; [mainWindow() updateTitleFor:self]; } } - (void)ircConnection:(IRCConnection *)sender willConnectToProxy:(NSString *)proxyHost port:(uint16_t)proxyPort { NSParameterAssert(sender == self.socket); IRCConnectionProxyType proxyType = self.socket.config.proxyType; if (proxyType == IRCConnectionProxyTypeSocks4) { [self printDebugInformationToConsole:TXTLS(@"IRC[p7h-un]", proxyHost, proxyPort)]; } else if (proxyType == IRCConnectionProxyTypeSocks5) { [self printDebugInformationToConsole:TXTLS(@"IRC[ni5-cy]", proxyHost, proxyPort)]; } else if (proxyType == IRCConnectionProxyTypeHTTP) { [self printDebugInformationToConsole:TXTLS(@"IRC[oby-av]", proxyHost, proxyPort)]; } } - (void)ircConnectionDidSecureConnection:(IRCConnection *)sender withProtocolType:(tls_protocol_version_t)protocolType cipherSuite:(tls_ciphersuite_t)cipherSuite { NSParameterAssert(sender == self.socket); NSString *protocolDescription = [RCMSecureTransport descriptionForProtocolType:protocolType]; NSString *cipherDescription = [RCMSecureTransport descriptionForCipherSuite:cipherSuite]; if (protocolDescription == nil || cipherDescription == nil) { return; } NSString *description = nil; if ([RCMSecureTransport isCipherSuiteDeprecated:cipherSuite] == NO) { description = TXTLS(@"IRC[uyz-4r]", protocolDescription, cipherDescription); } else { description = TXTLS(@"IRC[xwj-xy]", protocolDescription, cipherDescription); } [self printDebugInformationToConsole:TXTLS(@"IRC[ex4-f8]", description)]; } - (void)ircConnectionDidConnect:(IRCConnection *)sender { NSParameterAssert(sender == self.socket); if (self.isTerminating) { return; } [self startRetryTimer]; /* If the address we are connecting to is not an IP address, then we report back the actual IP address it was resolved to. */ NSString *connectedAddress = self.socket.connectedAddress; if (connectedAddress == nil || self.socket.config.serverAddress.IPAddress) { [self printDebugInformationToConsole:TXTLS(@"IRC[4vt-ow]")]; } else { [self printDebugInformationToConsole:TXTLS(@"IRC[l21-p7]", connectedAddress)]; } self.isConnecting = NO; self.isConnected = YES; self.userNickname = self.config.nickname; self.tryingNicknameSentNickname = self.config.nickname; [mainWindow() updateTitleFor:self]; [RZNotificationCenter() postNotificationName:IRCClientDidConnectNotification object:self]; NSString *username = self.config.username; NSString *realName = self.config.realName; NSString *modeSymbols = @"0"; NSString *serverPassword = self.server.serverPassword; if (self.config.setInvisibleModeOnConnect) { modeSymbols = @"8"; } if (username.length == 0) { username = self.config.nickname; } if (realName.length == 0) { realName = self.config.nickname; } [self sendCapability:@"LS" data:@"302"]; if (serverPassword) { [self sendPassword:serverPassword]; } [self changeNickname:self.tryingNicknameSentNickname]; [self send:@"USER", username, modeSymbols, @"*", realName, nil]; } - (void)ircConnection:(IRCConnection *)sender didDisconnectWithError:(nullable NSError *)disconnectError { NSParameterAssert(sender == self.socket); // if (self.isTerminating) { // return; // } [self changeStateOffWithError:disconnectError]; if (self.disconnectCallback) { self.disconnectCallback(); self.disconnectCallback = nil; } [RZNotificationCenter() postNotificationName:IRCClientDidDisconnectNotification object:self]; } - (void)ircConnectionDidCloseReadStream:(IRCConnection *)sender { NSParameterAssert(sender == self.socket); if (self.isTerminating) { return; } if (self.isDisconnecting) { return; } if (self.isQuitting) { [self disconnect]; return; } [self printDebugInformationToConsole:TXTLS(@"IRC[5h5-sl]")]; } /* This delegate call is not invoked on the main thread which means if it is modified to interact with UI, then it must invoke on the main thread eventually. */ - (void)ircConnection:(IRCConnection *)sender didReceiveData:(NSString *)data { NSParameterAssert(sender == self.socket); if (self.isConnected == NO || self.isTerminating) { return; } if (data.length == 0) { return; } self.lastMessageReceived = [NSDate timeIntervalSince1970]; worldController().bandwidthIn += data.length; worldController().messagesReceived += 1; [self rawDataLogIncomingTraffic:data]; if ([TPCPreferences removeAllFormatting]) { data = data.stripIRCEffects; } IRCMessage *message = [[IRCMessage alloc] initWithLine:data onClient:self]; if (message == nil) { return; } message = [THOPluginDispatcher interceptServerInput:message for:self]; if (message == nil) { return; } if ([self filterBatchCommandIncomingData:message]) { return; } [self processIncomingMessage:message]; } - (void)processIncomingMessageAttributes:(IRCMessage *)message { NSParameterAssert(message != nil); if (self.isLoggedIn == NO) { return; } if (message.isHistoric == NO) { return; } NSTimeInterval receivedTime = message.receivedAt.timeIntervalSince1970; if (receivedTime <= self.lastMessageServerTime) { return; } self.lastMessageServerTime = receivedTime; /* If the playback module is in use, then all messages are set as historic, so we set any lines above our current reference date as not historic to avoid collisions. */ if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityPlayback]) { [message markAsNotHistoric]; } } - (void)processIncomingMessage:(IRCMessage *)message { NSParameterAssert(message != nil); XRPerformBlockSynchronouslyOnMainQueue(^{ [self _processIncomingMessage:message]; }); } - (void)_processIncomingMessage:(IRCMessage *)message { NSParameterAssert(message != nil); [self processIncomingMessageAttributes:message]; if (message.commandNumeric > 0) { [self receiveNumericReply:message]; } else { NSUInteger commandNumeric = [IRCCommandIndex indexOfRemoteCommand:message.command]; switch (commandNumeric) { case IRCRemoteCommandNotice: // Command: NOTICE case IRCRemoteCommandPrivmsg: // Command: PRIVMSG { [self receivePrivmsgAndNotice:message]; break; } case IRCRemoteCommandError: // Command: ERROR { [self receiveError:message]; break; } case IRCRemoteCommandInvite: // Command: INVITE { [self receiveInvite:message]; break; } case IRCRemoteCommandJoin: // Command: JOIN { [self receiveJoin:message]; break; } case IRCRemoteCommandKick: // Command: KICK { [self receiveKick:message]; break; } case IRCRemoteCommandKill: // Command: KILL { [self receiveKill:message]; break; } case IRCRemoteCommandMode: // Command: MODE { [self receiveMode:message]; break; } case IRCRemoteCommandNick: // Command: NICK { [self receiveNick:message]; break; } case IRCRemoteCommandPart: // Command: PART { [self receivePart:message]; break; } case IRCRemoteCommandPing: // Command: PING { [self receivePing:message]; break; } case IRCRemoteCommandQuit: // Command: QUIT { [self receiveQuit:message]; break; } case IRCRemoteCommandTopic: // Command: TOPIC { [self receiveTopic:message]; break; } case IRCRemoteCommandWallops: // Command: WALLOPS { [self receiveWallops:message]; break; } case IRCRemoteCommandAuthenticate: // Command: AUTHENTICATE case IRCRemoteCommandCap: // Command: CAP { [self updateConnectedToZNCPropertyWithMessage:message]; [self receiveCapabilityOrAuthenticationRequest:message]; break; } case IRCRemoteCommandAway: // Command: AWAY (away-notify CAP) { [self receiveAwayNotifyCapability:message]; break; } case IRCRemoteCommandBatch: // BATCH { [self receiveBatch:message]; break; } case IRCRemoteCommandCertinfo: // CERTINFO { [self receiveCertInfo:message]; break; } case IRCRemoteCommandChghost: { [self receiveChangeHost:message]; break; } } // switch } [self processBundlesServerMessage:message]; } - (void)ircConnection:(IRCConnection *)sender willSendData:(NSString *)data { NSParameterAssert(sender == self.socket); if (self.isTerminating) { return; } [self rawDataLogOutgoingTraffic:data]; } #pragma mark - #pragma mark Raw Data Logging - (void)createRawDataLogQuery { if (self.isTerminating) { return; } if (self.rawDataLogQuery != nil) { return; } IRCChannel *query = [self findChannelOrCreate:@"Server Traffic" isUtility:YES]; self.rawDataLogQuery = query; [mainWindow() select:query]; [self rawDataLog:TXTLS(@"IRC[ik6-dl]")]; } - (void)destroyRawDataLogQuery { if (self.isTerminating) { return; } if (self.rawDataLogQuery == nil) { return; } [worldController() destroyChannel:self.rawDataLogQuery]; } - (void)rawDataLog:(NSString *)data { NSParameterAssert(data != nil); if (self.isTerminating) { return; } XRPerformBlockSynchronouslyOnMainQueue(^{ [self printDebugInformation:data inChannel:self.rawDataLogQuery]; }); } - (void)rawDataLogOutgoingTraffic:(NSString *)data { NSParameterAssert(data != nil); if (self.rawDataLogQuery == nil) { return; } NSString *dataToLog = [NSString stringWithFormat:@"<< %@", data]; [self rawDataLog:dataToLog]; } - (void)rawDataLogIncomingTraffic:(NSString *)data { NSParameterAssert(data != nil); if (self.rawDataLogQuery == nil) { return; } NSString *dataToLog = [NSString stringWithFormat:@">> %@", data]; [self rawDataLog:dataToLog]; } #pragma mark - #pragma mark NickServ Information - (NSArray *)nickServSupportedNeedIdentificationTokens { return [TPCResourceManager arrayFromResources:@"StaticStore" key:@"IRCClient List of NickServ Needs Identification Tokens"]; } - (NSArray *)nickServSupportedSuccessfulIdentificationTokens { return [TPCResourceManager arrayFromResources:@"StaticStore" key:@"IRCClient List of NickServ Successfully Identified Tokens"]; } #pragma mark - #pragma mark Protocol Handlers - (void)receiveWallops:(IRCMessage *)m { NSParameterAssert(m != nil); /* WALLOPS are rewritten so that they can be parsed as regular notices */ NSMutableArray *paramsMutable = [m.params mutableCopy]; [paramsMutable insertObject:self.userNickname atIndex:0]; NSString *text = [NSString stringWithFormat:TVCLogLineSpecialNoticeMessageFormat, m.command, paramsMutable[1]]; paramsMutable[1] = text; /* ======================================== */ IRCMessageMutable *messageMutable = [m mutableCopy]; messageMutable.command = @"NOTICE"; messageMutable.params = paramsMutable; /* ======================================== */ [self receivePrivmsgAndNotice:[messageMutable copy]]; } - (void)receivePrivmsgAndNotice:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] > 1); NSString *text = [m paramAt:1]; if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIdentifyCTCP] && ([text hasPrefix:@"+\x01"] || [text hasPrefix:@"-\x01"])) { text = [text substringFromIndex:1]; } else if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIdentifyMsg] && ([text hasPrefix:@"+"] || [text hasPrefix:@"-"])) { text = [text substringFromIndex:1]; } TVCLogLineType lineType = TVCLogLineTypePrivateMessage; BOOL isPlainText = [m.command isEqualToString:@"PRIVMSG"]; if ([text hasPrefix:@"\x01"]) { text = [text substringFromIndex:1]; NSInteger closingIndex = [text stringPosition:@"\x01"]; if (closingIndex >= 0) { text = [text substringToIndex:closingIndex]; } if (isPlainText) { if ([text hasPrefixIgnoringCase:@"ACTION "]) { text = [text substringFromIndex:7]; lineType = TVCLogLineTypeAction; } else { lineType = TVCLogLineTypeCTCPQuery; } } else { lineType = TVCLogLineTypeCTCPReply; // notice -> query } } else if (isPlainText == NO) { lineType = TVCLogLineTypeNotice; } if (lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypeNotice) { [self receiveText:m lineType:lineType text:text]; } else if (lineType == TVCLogLineTypeCTCPQuery) { [self receiveCTCPQuery:m text:text]; } else if (lineType == TVCLogLineTypeCTCPReply) { [self receiveCTCPReply:m text:text]; } } - (void)receiveText:(IRCMessage *)m lineType:(TVCLogLineType)lineType text:(NSString *)text { NSParameterAssert(m != nil); NSParameterAssert(lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeActionNoHighlight || lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypePrivateMessageNoHighlight || lineType == TVCLogLineTypeNotice); NSParameterAssert(text != nil); NSAssertReturn([m paramsCount] > 1); /* Allow empty actions but no other type */ if (text.length == 0) { if (lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeActionNoHighlight) { text = @" "; } else { return; } } /* Process target */ NSString *target = [m paramAt:0]; if (target.length == 0) { return; } /* It is possible for a channel to have a user mode character in front of it when the message is being addressed to a specific group of users. For example, "+#channel" as the channel name means that the message is addressed to only the voiced users of #channel. We don't care about this mode but we still have to remove it while also taking into account channels who use a character other than the pound symbol as their prefix. */ NSString *targetPrefix = [self.supportInfo extractStatusMessagePrefixFromChannelNamed:target]; if (targetPrefix.length == 1) { target = [target substringFromIndex:1]; } /* Perform ignore check */ IRCAddressBookEntry *ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask]; if (ignoreInfo.ignorePublicMessageHighlights) { if (lineType == TVCLogLineTypeAction) { lineType = TVCLogLineTypeActionNoHighlight; } else if (lineType == TVCLogLineTypePrivateMessage) { lineType = TVCLogLineTypePrivateMessageNoHighlight; } } if (lineType == TVCLogLineTypeNotice) { if (ignoreInfo.ignoreNoticeMessages) { return; } } /* Even though OTR doesn't allow channel decryption, we still wrap everything in a decryption block because Blowfish plugin may swizzle the logic. That plugin does support channel decryption. */ BOOL performDecryption = YES; TLOEncryptionManagerEncodingDecodingCallbackBlock decryptionBlock = nil; /* Public message (directed at channel) */ if ([self stringIsChannelName:target]) { if (ignoreInfo.ignorePublicMessages) { return; } decryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) { [self _receiveText_Public:m lineType:lineType target:target text:originalString wasEncrypted:wasEncrypted]; }; } /* Private message (from user) */ else if (m.senderIsServer == NO) { if (ignoreInfo.ignorePrivateMessages) { return; } decryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) { [self _receiveText_Private:m lineType:lineType target:target text:originalString wasEncrypted:wasEncrypted]; }; } /* Private message (from server) */ else { /* It is not possible to hold an OTR conversation with a server. */ performDecryption = NO; decryptionBlock = ^(NSString *originalString, BOOL wasEncrypted) { [self _receiveText_PrivateServer:m lineType:lineType target:target text:originalString wasEncrypted:wasEncrypted]; }; } /* Perform decryption */ if (performDecryption) { NSString *sender = m.senderNickname; [self decryptMessage:text from:sender target:target decodingCallback:decryptionBlock]; } else { decryptionBlock(text, NO); } } - (void)_receiveText_Public:(IRCMessage *)m lineType:(TVCLogLineType)lineType target:(NSString *)target text:(NSString *)text wasEncrypted:(BOOL)wasEncrypted { NSParameterAssert(m != nil); NSParameterAssert(target != nil); NSParameterAssert(text != nil); IRCChannel *channel = [self findChannel:target]; if (channel == nil) { return; } NSString *sender = m.senderNickname; BOOL isSelfMessage = NO; if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage]) { isSelfMessage = [self nicknameIsMyself:sender]; } TVCLogControllerPrintOperationCompletionBlock printCompletionBlock = nil; BOOL isPlainText = (lineType != TVCLogLineTypeNotice); if (isPlainText == NO) { /* Completion block for notices */ printCompletionBlock = ^(TVCLogControllerPrintOperationContext *context) { if ([self isSafeToPostNotificationForMessage:m inChannel:channel]) { [self notifyText:TXNotificationTypeChannelNotice lineType:lineType target:channel nickname:sender text:text]; } }; } else { /* Completion block for regular messages */ printCompletionBlock = ^(TVCLogControllerPrintOperationContext *context) { if (isSelfMessage) { return; } BOOL isHighlight = context.highlight; BOOL postEvent = YES; if ([self isSafeToPostNotificationForMessage:m inChannel:channel]) { if (isHighlight) { postEvent = [self notifyText:TXNotificationTypeHighlight lineType:lineType target:channel nickname:sender text:text]; } else { postEvent = [self notifyText:TXNotificationTypeChannelMessage lineType:lineType target:channel nickname:sender text:text]; } } if (postEvent == NO) { return; } if (isHighlight) { [self setHighlightStateForChannel:channel]; } [self setUnreadStateForChannel:channel isHighlight:isHighlight]; }; } /* Ask for permission to print message */ BOOL printMessage = YES; if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent]) { printMessage = [THOPluginDispatcher receivedText:text authoredBy:m.sender destinedFor:channel asLineType:lineType onClient:self receivedAt:m.receivedAt wasEncrypted:wasEncrypted]; } /* Print message */ if (printMessage) { [self print:text by:sender inChannel:channel asType:lineType command:m.command receivedAt:m.receivedAt isEncrypted:wasEncrypted referenceMessage:m completionBlock:printCompletionBlock]; } /* The remaining logic does not apply to notices */ if (isPlainText == NO) { return; } /* Update weights of user we're talking with */ IRCChannelUser *senderMember = [channel findMember:sender]; if (senderMember == nil) { return; } NSString *localNickname = [self.userNickname trimCharacters:@"_"]; // Remove any underscores from around nickname (Guest___ becomes Guest) /* If we are mentioned in this piece of text, then update our weight for the user */ if ([text contains:localNickname]) { [senderMember outgoingConversation]; } else { [senderMember conversation]; } } - (void)_receiveText_Private:(IRCMessage *)m lineType:(TVCLogLineType)lineType target:(NSString *)target text:(NSString *)text wasEncrypted:(BOOL)wasEncrypted { NSParameterAssert(m != nil); NSParameterAssert(target != nil); NSParameterAssert(text != nil); NSString *sender = m.senderNickname; TVCLogControllerPrintOperationCompletionBlock printCompletionBlock = nil; BOOL isPlainText = (lineType != TVCLogLineTypeNotice); /* If the self-message CAP is not enabled, we still check if we are on a ZNC based connections because older versions of ZNC combined with the privmsg module need the correct behavior which the self-message CAP evolved into. */ BOOL isSelfMessage = NO; if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage] || [self isCapabilityEnabled:ClientIRCv3SupportedCapabilityZNCSelfMessage] || self.isConnectedToZNC) { isSelfMessage = [self nicknameIsMyself:sender]; } /* Does the query for the sender already exist?... */ IRCChannel *query = nil; if (isSelfMessage) { query = [self findChannel:target]; // Look for a query related to target, rather than sender } else { query = [self findChannel:sender]; } BOOL newPrivateMessage = NO; if (isPlainText == NO) { /* Logic for notices */ /* Process services */ if ([sender isEqualToStringIgnoringCase:@"ChanServ"]) { [self _receiveText_PrivateNoticeFromChanServ:&query text:&text]; } else if ([sender isEqualToStringIgnoringCase:@"NickServ"]) { [self _receiveText_PrivateNoticeFromNickServ:&query text:&text]; } /* Determine where to send notice messages */ if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationSelectedChannel) { query = [mainWindow() selectedChannelOn:self]; } /* Do not create query until after ChanServ notices have been processed so that entry messages do not create a new window. */ if (query == nil) { if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationQuery) { // newPrivateMessage = YES; if (isSelfMessage) { query = [worldController() createPrivateMessage:target onClient:self]; } else { query = [worldController() createPrivateMessage:sender onClient:self]; } } } printCompletionBlock = ^(TVCLogControllerPrintOperationContext *context) { if (isSelfMessage) { return; } BOOL postEvent = YES; if ([self isSafeToPostNotificationForMessage:m inChannel:query]) { postEvent = [self notifyText:TXNotificationTypePrivateNotice lineType:lineType target:query nickname:sender text:text]; } if (postEvent && query != nil) { [self setUnreadStateForChannel:query]; } }; } else // NOTICE message { /* Logic for regular messages */ if (query == nil) { newPrivateMessage = YES; if (isSelfMessage) { query = [worldController() createPrivateMessage:target onClient:self]; } else { query = [worldController() createPrivateMessage:sender onClient:self]; } } printCompletionBlock = ^(TVCLogControllerPrintOperationContext *context) { if (isSelfMessage) { return; } BOOL isHighlight = context.highlight; BOOL postEvent = YES; if ([self isSafeToPostNotificationForMessage:m inChannel:query]) { if (isHighlight) { postEvent = [self notifyText:TXNotificationTypeHighlight lineType:lineType target:query nickname:sender text:text]; } else { if (newPrivateMessage) { postEvent = [self notifyText:TXNotificationTypeNewPrivateMessage lineType:lineType target:query nickname:sender text:text]; } else { postEvent = [self notifyText:TXNotificationTypePrivateMessage lineType:lineType target:query nickname:sender text:text]; } } } if (postEvent == NO) { return; } if (isHighlight) { [self setHighlightStateForChannel:query]; } [self setUnreadStateForChannel:query isHighlight:isHighlight]; }; } /* Ask for permission to print message */ BOOL printMessage = YES; if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent]) { printMessage = [THOPluginDispatcher receivedText:text authoredBy:m.sender destinedFor:query asLineType:lineType onClient:self receivedAt:m.receivedAt wasEncrypted:wasEncrypted]; } /* Print message */ if (printMessage) { [self print:text by:sender inChannel:query asType:lineType command:m.command receivedAt:m.receivedAt isEncrypted:wasEncrypted referenceMessage:m completionBlock:printCompletionBlock]; } /* The remaining logic does not apply to notices */ if (isPlainText == NO) { return; } /* Update query status */ if (query.isActive == NO) { [query activate]; [mainWindow() reloadTreeItem:query]; } } - (void)_receiveText_PrivateNoticeFromChanServ:(IRCChannel **)target text:(NSString **)text { NSParameterAssert(target != NULL); NSParameterAssert(text != NULL); NSString *textIn = (*text); /* Forward entry messages to the channel they are associated with. */ /* Format we are going for: -ChanServ- [#channelname] blah blah... */ NSInteger spacePosition = [textIn stringPosition:@" "]; if ([textIn hasPrefix:@"["] == NO || spacePosition < 4) { return; } NSString *textHead = [textIn substringToIndex:spacePosition]; if ([textHead hasSuffix:@"]"] == NO) { return; } textHead = [textHead substringToIndex:(textHead.length - 1)]; // Remove the ] textHead = [textHead substringFromIndex:1]; // Remove the [ if ([self stringIsChannelName:textHead] == NO) { return; } IRCChannel *channel = [self findChannel:textHead]; if (channel == nil) { return; } *text = [textIn substringFromIndex:(textHead.length + 2)]; // Remove the [#channelname] from the text *target = channel; } - (void)_receiveText_PrivateNoticeFromNickServ:(IRCChannel **)target text:(NSString **)text { NSParameterAssert(target != NULL); NSParameterAssert(text != NULL); self.serverHasNickServ = YES; NSString *textIn = nil; if ([TPCPreferences removeAllFormatting] == NO) { textIn = (*text).stripIRCEffects; } else { textIn = (*text); } /* If we are not waiting for a response from NickServ, then try sending our password if that's what it requested. */ if (self.isWaitingForNickServ == NO) { NSString *nicknamePassword = self.config.nicknamePassword; if (nicknamePassword.length == 0) { return; } for (NSString *token in self.nickServSupportedNeedIdentificationTokens) { if ([textIn containsIgnoringCase:token] == NO) { continue; } // Send password if ([self.serverAddress hasSuffix:@".dal.net"]) { NSString *message = [NSString stringWithFormat:@"IDENTIFY %@", nicknamePassword]; [self send:@"PRIVMSG", @"NickServ@services.dal.net", message, nil]; } else if (self.config.sendAuthenticationRequestsToUserServ) { NSString *message = [NSString stringWithFormat:@"login %@ %@", self.config.nickname, nicknamePassword]; [self send:@"PRIVMSG", @"userserv", message, nil]; } else { NSString *message = [NSString stringWithFormat:@"IDENTIFY %@", nicknamePassword]; [self send:@"PRIVMSG", @"NickServ", message, nil]; } // Reset properties self.isWaitingForNickServ = YES; self.userIsIdentifiedWithNickServ = NO; break; } return; } /* Scan for messages telling us that we are now identified */ for (NSString *token in self.nickServSupportedSuccessfulIdentificationTokens) { if ([textIn containsIgnoringCase:token] == NO) { continue; } self.isWaitingForNickServ = NO; self.userIsIdentifiedWithNickServ = YES; if (self.config.autojoinWaitsForNickServ) { [self performAutoJoin]; } break; } } - (void)_receiveText_PrivateServer:(IRCMessage *)m lineType:(TVCLogLineType)lineType target:(NSString *)target text:(NSString *)text wasEncrypted:(BOOL)wasEncrypted { NSParameterAssert(m != nil); NSParameterAssert(target != nil); NSParameterAssert(text != nil); NSString *sender = m.senderNickname; BOOL isPlainText = (lineType != TVCLogLineTypeNotice); IRCChannel *query = nil; /* For notices, send to a query if a query for the server already exists. Otherwise, it is always sent to the console. Plain text messages always create a new query but does not post a notification. */ if (isPlainText == NO) { query = [self findChannel:sender]; } else { // NOTICE message query = [self findChannelOrCreate:sender isPrivateMessage:YES]; } /* Print message */ BOOL printMessage = YES; if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureDidReceivePlainTextMessageEvent]) { printMessage = [THOPluginDispatcher receivedText:text authoredBy:m.sender destinedFor:query asLineType:lineType onClient:self receivedAt:m.receivedAt wasEncrypted:wasEncrypted]; } if (printMessage) { [self print:text by:sender inChannel:query asType:lineType command:m.command receivedAt:m.receivedAt isEncrypted:wasEncrypted]; } /* Disconnect and reconnect if message is believed to be from an irssi proxy */ /* If we do not do this, the internal state of the client becomes fucked all around */ if ([sender hasSuffix:@".proxy"] && [text isEqualToString:@"Connected to server"]) { __weak IRCClient *weakSelf = self; self.disconnectCallback = ^{ [weakSelf printDebugInformationToConsole:TXTLS(@"IRC[5i4-qq]")]; [weakSelf connect:IRCClientConnectModeReconnect]; }; [self disconnect]; } } - (void)receiveCTCPQuery:(IRCMessage *)m text:(NSString *)text { NSParameterAssert(m != nil); NSParameterAssert(text != nil); NSString *sender = m.senderNickname; BOOL myself = [self nicknameIsMyself:sender]; IRCAddressBookEntry *ignoreInfo = nil; if (myself) { /* Ignore messages echoed back to ourselves */ if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage]) { return; } } else { /* Find ignore for sender and possibly exit method */ ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask]; if (ignoreInfo.ignoreClientToClientProtocol) { return; } } /* Context */ NSMutableString *textMutable = [text mutableCopy]; NSString *command = textMutable.uppercaseGetToken; if (command.length == 0) { return; } /* Lag check responses should only ever come from ourselves so we let it through. The method we call into already has a built-in check for myself so no need to wrap an if statement here. */ if ([command isEqualToString:@"LAGCHECK"]) { [self receiveCTCPLagCheckQuery:m text:textMutable]; return; } /* Ignore query if the user has configured Textual to do so */ if ([TPCPreferences replyToCTCPRequests] == NO) { [self printDebugInformationToConsole:TXTLS(@"IRC[bg3-h2]", command, sender)]; return; } /* Process DCC requests elsewhere */ if ([command isEqualToString:@"DCC"]) { [self receivedDCCQuery:m text:textMutable ignoreInfo:ignoreInfo]; return; } /* Print message */ IRCChannel *printTarget = nil; if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationSelectedChannel) { printTarget = [mainWindow() selectedChannelOn:self]; } NSString *messageToPrint = TXTLS(@"IRC[6o8-eu]", command, sender); [self print:messageToPrint by:nil inChannel:printTarget asType:TVCLogLineTypeCTCPQuery command:m.command receivedAt:m.receivedAt]; /* CLIENTINFO command */ if ([command isEqualToString:@"CLIENTINFO"]) { [self sendCTCPReply:sender command:command text:TXTLS(@"IRC[jer-ju]")]; } /* FINGER command */ else if ([command isEqualToString:@"FINGER"]) { [self sendCTCPReply:sender command:command text:TXTLS(@"IRC[en6-mw]")]; } /* PING command */ else if ([command isEqualToString:@"PING"]) { if (textMutable.length > 50) { LogToConsoleFault("Ignoring PING query that exceeds 50 bytes"); return; } [self sendCTCPReply:sender command:command text:textMutable]; } /* TIME command */ else if ([command isEqualToString:@"TIME"]) { NSDateFormatter *dateFormatter = TXSharedISOStandardDateFormatter(); NSString *text = [dateFormatter stringFromDate:[NSDate date]]; [self sendCTCPReply:sender command:command text:text]; } /* USERINFO command */ else if ([command isEqualToString:@"USERINFO"]) { [self sendCTCPReply:sender command:command text:self.config.realName]; } /* VERSION command */ else if ([command isEqualToString:@"VERSION"]) { NSString *fakeVersion = [TPCPreferences masqueradeCTCPVersion]; if (fakeVersion.length > 0) { [self sendCTCPReply:sender command:command text:fakeVersion]; return; } NSString *applicationName = [TPCApplicationInfo applicationNameWithoutVersion]; NSString *versionShort = [TPCApplicationInfo applicationVersionShort]; NSString *text = TXTLS(@"IRC[vzu-u7]", applicationName, versionShort); [self sendCTCPReply:sender command:command text:text]; } } - (void)receiveCTCPLagCheckQuery:(IRCMessage *)m text:(NSString *)text { NSParameterAssert(m != nil); if ([self messageIsFromMyself:m] == NO) { return; } NSDictionary *lagCheckContext = [text formDataUsingSeparator:@"&"]; if (lagCheckContext.count == 0) { return; } NSString *connectionIdentifier = lagCheckContext[@"connection"]; if ([connectionIdentifier isEqualToString:self.socket.uniqueIdentifier] == NO) { /* We check which connection this event is linked to so that we ignore events after reconnects (such as from a bouncer). */ return; } NSTimeInterval firstTime = [lagCheckContext doubleForKey:@"time"]; double delta = (([NSDate timeIntervalSince1970] - firstTime) * 1000); NSString *ratingString = nil; if (delta < 10) { // Yeah, okay… ratingString = TXTLS(@"IRC[58g-m9]"); } else if (delta > 10 && delta <= 25) { // Are you plugged into the server? ratingString = TXTLS(@"IRC[0jp-93]"); } else if (delta > 25 && delta <= 100) { // Pretty good ratingString = TXTLS(@"IRC[yym-8y]"); } else if (delta > 100 && delta <= 125) { // Not bad ratingString = TXTLS(@"IRC[mic-qe]"); } else if (delta > 125 && delta <= 200) { // Okay ratingString = TXTLS(@"IRC[mqg-wi]"); } else if (delta > 200 && delta <= 225) { // Needs work ratingString = TXTLS(@"IRC[ut8-7s]"); } else if (delta > 225 && delta <= 300) { // Slow ratingString = TXTLS(@"IRC[8fo-ss]"); } else if (delta > 300) { // Very Slow ratingString = TXTLS(@"IRC[4oc-p2]"); } NSString *message = TXTLS(@"IRC[5bf-jp]", self.serverAddress, delta, ratingString); NSString *channelName = lagCheckContext[@"channel"]; IRCChannel *channel = nil; if (channelName) { channel = [self findChannel:channelName]; } if (channel) { [self sendPrivmsg:message toChannel:channel]; } else { [self printDebugInformation:message]; } } - (void)receiveCTCPReply:(IRCMessage *)m text:(NSString *)text { NSParameterAssert(m != nil); NSParameterAssert(text != nil); /* Find ignore for sender and possibly exit method */ IRCAddressBookEntry *ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask]; if (ignoreInfo.ignoreClientToClientProtocol) { return; } /* Context */ NSMutableString *textMutable = [text mutableCopy]; NSString *sender = m.senderNickname; NSString *command = textMutable.uppercaseGetToken; if (command.length == 0) { return; } /* Print message */ IRCChannel *printTarget = nil; if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationSelectedChannel) { printTarget = [mainWindow() selectedChannelOn:self]; } NSString *messageToPrint = nil; if ([command isEqualToString:@"PING"]) { double delta = ([NSDate timeIntervalSince1970] - textMutable.doubleValue); messageToPrint = TXTLS(@"IRC[vy7-pk]", sender, command, delta); } else { messageToPrint = TXTLS(@"IRC[dri-l7]", sender, command, textMutable); } [self print:messageToPrint by:nil inChannel:printTarget asType:TVCLogLineTypeCTCPReply command:m.command receivedAt:m.receivedAt]; } - (void)receiveJoin:(IRCMessage *)m { NSAssertReturn([m paramsCount] > 0); BOOL isPrintOnlyMessage = m.isPrintOnlyMessage; NSString *sender = m.senderNickname; BOOL myself = [self nicknameIsMyself:sender]; NSString *channelName = [m paramAt:0]; IRCChannel *channel = nil; if (isPrintOnlyMessage == NO && myself) { channel = [self findChannelOrCreate:channelName]; if (channel.isActive == NO && channel.isChannel) { [channel activate]; } else { return; } self.userHostmask = m.senderHostmask; [mainWindow() reloadTreeItem:channel]; } else // myself { channel = [self findChannel:channelName]; if (channel == nil || channel.isChannel == NO) { return; } } if (isPrintOnlyMessage == NO) { /* A user might already exist by having a private message open */ IRCUserMutable *userMutable = [self mutableCopyOfUserWithNickname:sender]; userMutable.nickname = m.senderNickname; userMutable.username = m.senderUsername; userMutable.address = m.senderAddress; IRCUser *userAdded = [self addUserAndReturn:userMutable]; IRCChannelUser *member = [[IRCChannelUser alloc] initWithUser:userAdded]; [channel addMember:member checkForDuplicates:YES]; } if (isPrintOnlyMessage == NO && myself == NO) { IRCChannel *senderQuery = [self findChannel:sender]; if (senderQuery && senderQuery.isActive == NO) { [senderQuery activate]; [self print:TXTLS(@"IRC[q0q-ch]", sender) by:nil inChannel:senderQuery asType:TVCLogLineTypeJoin command:m.command receivedAt:m.receivedAt]; [mainWindow() reloadTreeItem:senderQuery]; } } IRCAddressBookEntry *ignoreInfo = nil; if (myself == NO) { ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask]; if (ignoreInfo && isPrintOnlyMessage == NO) { [self updateUserTrackingStatusForEntry:ignoreInfo withMessage:m]; } } BOOL printMessage = [self postReceivedMessage:m withText:nil destinedFor:channel]; if (printMessage && myself == NO) { if ([TPCPreferences showJoinLeave] == NO) { printMessage = NO; } else if (channel.config.ignoreGeneralEventMessages) { printMessage = NO; } else if (ignoreInfo) { printMessage = (ignoreInfo.ignoreGeneralEventMessages == NO); } } if (printMessage) { NSString *message = TXTLS(@"IRC[ziu-p9]", sender, m.senderUsername, m.senderAddress.stringByAppendingIRCFormattingStop); [self print:message by:nil inChannel:channel asType:TVCLogLineTypeJoin command:m.command receivedAt:m.receivedAt]; } if (isPrintOnlyMessage) { return; } [mainWindow() updateTitleFor:channel]; if (myself) { if (self.config.sendWhoCommandRequestsToChannels && self.isBrokenIRCd_aka_Twitch == NO) { [self requestModesForChannel:channel]; } } else { [self notifyEvent:TXNotificationTypeUserJoined lineType:TVCLogLineTypeJoin target:channel nickname:sender text:nil]; } } - (void)receivePart:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] > 0); /* ZNC sends PART messages for every channel when the client disconnects to force it to update its local status. This is incredibly misleading to the user, as they see the message that they left the channel and believe they did. This condition filters out these messages. Because Textual is intelligent enough to clear status related to channels when the connection is quit, this is safe. */ if (self.isQuitting && self.isConnectedToZNC) { return; } BOOL isPrintOnlyMessage = m.isPrintOnlyMessage; NSString *channelName = [m paramAt:0]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil || channel.isChannel == NO) { return; } NSString *sender = m.senderNickname; NSString *comment = [m paramAt:1]; BOOL myself = [self nicknameIsMyself:sender]; if (isPrintOnlyMessage == NO) { if (myself) { [channel deactivate]; [mainWindow() reloadTreeItem:channel]; } else { [channel removeMemberWithNickname:sender]; /* Notify user */ [self notifyEvent:TXNotificationTypeUserParted lineType:TVCLogLineTypePart target:channel nickname:sender text:comment]; } } BOOL printMessage = [self postReceivedMessage:m withText:comment destinedFor:channel]; if (printMessage && myself == NO) { if ([TPCPreferences showJoinLeave] == NO) { printMessage = NO; } else if (channel.config.ignoreGeneralEventMessages) { printMessage = NO; } else { IRCAddressBookEntry *ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask]; if (ignoreInfo) { printMessage = (ignoreInfo.ignoreGeneralEventMessages == NO); } } } if (printMessage) { NSString *message = TXTLS(@"IRC[nkr-kf]", sender, m.senderUsername, m.senderAddress.stringByAppendingIRCFormattingStop); if (comment.length > 0) { message = TXTLS(@"IRC[ozy-6i]", message, comment.stringByAppendingIRCFormattingStop); } [self print:message by:nil inChannel:channel asType:TVCLogLineTypePart command:m.command receivedAt:m.receivedAt]; } if (isPrintOnlyMessage == NO) { [mainWindow() updateTitleFor:channel]; } } - (void)receiveKick:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] > 1); BOOL isPrintOnlyMessage = m.isPrintOnlyMessage; NSString *channelName = [m paramAt:0]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil || channel.isChannel == NO) { return; } NSString *sender = m.senderNickname; NSString *target = [m paramAt:1]; NSString *comment = [m paramAt:2]; BOOL myself = [self nicknameIsMyself:target]; if (isPrintOnlyMessage == NO) { if (myself) { [channel deactivate]; [mainWindow() reloadTreeItem:channel]; /* Notify user */ [self notifyEvent:TXNotificationTypeKick lineType:TVCLogLineTypeKick target:channel nickname:sender text:comment]; /* Rejoin channel */ if ([TPCPreferences rejoinOnKick] && channel.errorOnLastJoinAttempt == NO) { [self printDebugInformation:TXTLS(@"IRC[zzj-2h]") inChannel:channel]; [self cancelPerformRequestsWithSelector:@selector(joinKickedChannel:) object:channel]; [self performSelectorInCommonModes:@selector(joinKickedChannel:) withObject:channel afterDelay:3.0]; } } else // myself { [channel removeMemberWithNickname:target]; } } BOOL printMessage = [self postReceivedMessage:m withText:comment destinedFor:channel]; if (printMessage && myself == NO) { if ([TPCPreferences showJoinLeave] == NO) { printMessage = NO; } else if (channel.config.ignoreGeneralEventMessages) { printMessage = NO; } else { IRCAddressBookEntry *ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask]; if (ignoreInfo) { printMessage = (ignoreInfo.ignoreGeneralEventMessages == NO); } } } if (printMessage) { NSString *message = TXTLS(@"IRC[9aj-bd]", sender, target, comment.stringByAppendingIRCFormattingStop); [self print:message by:nil inChannel:channel asType:TVCLogLineTypeKick command:m.command receivedAt:m.receivedAt]; } if (isPrintOnlyMessage == NO) { [mainWindow() updateTitleFor:channel]; } } - (void)receiveQuit:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] > 0); BOOL isPrintOnlyMessage = m.isPrintOnlyMessage; NSString *channelName = nil; NSString *comment = nil; if (isPrintOnlyMessage) { channelName = [m paramAt:0]; comment = [m paramAt:1]; } else { comment = [m paramAt:0]; } NSString *sender = m.senderNickname; BOOL myself = [self nicknameIsMyself:sender]; IRCUser *user = nil; if (isPrintOnlyMessage == NO) { user = [self findUser:sender]; if (user == nil) { return; } } IRCAddressBookEntry *ignoreInfo = nil; if (myself == NO) { ignoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask]; if (ignoreInfo && isPrintOnlyMessage == NO) { [self updateUserTrackingStatusForEntry:ignoreInfo withMessage:m]; } } NSString *messageToPrint = TXTLS(@"IRC[53b-dm]", sender, m.senderUsername, m.senderAddress.stringByAppendingIRCFormattingStop); if (comment.length > 0) { messageToPrint = TXTLS(@"IRC[tok-st]", messageToPrint, comment.stringByAppendingIRCFormattingStop); } void (^printingBlock)(IRCChannel *) = ^(IRCChannel *channel) { if (myself == NO && isPrintOnlyMessage == NO) { switch (channel.type) { case IRCChannelTypeChannel: { IRCChannelUser *member = [user userAssociatedWithChannel:channel]; if (member == nil) { return; } [channel removeMember:member]; break; } case IRCChannelTypePrivateMessage: { if ([sender isEqualToStringIgnoringCase:channel.name] == NO) { return; } if (channel.isActive) { [channel deactivate]; [mainWindow() reloadTreeItem:channel]; } break; } default: { return; } } } NSString *message = messageToPrint; if (channel.isChannel) { BOOL printMessage = [self postReceivedMessage:m withText:comment destinedFor:channel]; if (printMessage && myself == NO) { if ([TPCPreferences showJoinLeave] == NO) { printMessage = NO; } else if (channel.config.ignoreGeneralEventMessages) { printMessage = NO; } else if (ignoreInfo) { printMessage = (ignoreInfo.ignoreGeneralEventMessages == NO); } } [mainWindow() updateTitleFor:channel]; if (printMessage == NO) { return; } } else // -isChannel { message = TXTLS(@"IRC[8bk-mx]", sender); } [self print:message by:nil inChannel:channel asType:TVCLogLineTypeQuit command:m.command receivedAt:m.receivedAt]; }; if (isPrintOnlyMessage) { IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { return; } printingBlock(channel); return; } for (IRCChannel *c in self.channelList) { printingBlock(c); } if (myself == NO) { [mainWindow() updateTitleFor:self]; [self notifyEvent:TXNotificationTypeUserDisconnected lineType:TVCLogLineTypeQuit target:nil nickname:sender text:comment]; } } - (void)receiveKill:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] > 0); NSString *nickname = [m paramAt:0]; for (IRCChannel *c in self.channelList) { [c removeMemberWithNickname:nickname]; } } - (void)receiveNick:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] == 1); /* Print only messages target specific channels which means the index of incoming data will be different */ BOOL isPrintOnlyMessage = m.isPrintOnlyMessage; NSString *channelName = nil; NSString *newNickname = nil; if (isPrintOnlyMessage) { channelName = [m paramAt:0]; newNickname = [m paramAt:1]; } else { newNickname = [m paramAt:0]; } /* There's no reason to perform an update if nothing changed */ NSString *oldNickname = m.senderNickname; if ([oldNickname isEqualToString:newNickname]) { return; } BOOL myself = [self nicknameIsMyself:oldNickname]; /* Find address book entry for old nickname and update tracking status. This entry will also be used later on, when printing, to decide whether to print the message. */ IRCAddressBookEntry *oldNicknameIgnoreInfo = nil; if (myself == NO) { oldNicknameIgnoreInfo = [self findAddressBookEntryForHostmask:m.senderHostmask]; } /* Perform restricted actions */ if (isPrintOnlyMessage == NO) { if (myself) { self.userNickname = newNickname; if (self.tryingNicknameSentNickname != nil) { self.tryingNicknameSentNickname = newNickname; } /* Reload window title (our nickname is shown there) */ [mainWindow() updateTitleFor:self]; } else { /* Update user tracking status for old nickname */ if (oldNicknameIgnoreInfo) { [self updateUserTrackingStatusForEntry:oldNicknameIgnoreInfo withMessage:m]; } /* Update user tracking status for new nickname */ IRCAddressBookEntry *newNicknameIgnoreInfo = [self findUserTrackingAddressBookEntryForNickname:newNickname]; if (newNicknameIgnoreInfo) { [self updateUserTrackingStatusForEntry:newNicknameIgnoreInfo withMessage:m]; } } /* Inform style of change */ [self postEventToViewController:@"nicknameChanged"]; } /* Inform observers */ [RZNotificationCenter() postNotificationName:IRCClientUserNicknameChangedNotification object:self userInfo:@{ @"oldNickname" : oldNickname, @"newNickname" : newNickname }]; /* Look for user */ IRCUser *user = nil; if (isPrintOnlyMessage == NO) { user = [self findUser:oldNickname]; if (user == nil) { return; } } /* Setup block that is used by printing operations */ NSString *messageToPrint = nil; if (myself) { messageToPrint = TXTLS(@"IRC[rr6-yo]", newNickname); } else { messageToPrint = TXTLS(@"IRC[fxw-5s]", oldNickname, newNickname); } void (^printingBlock)(IRCChannel *) = ^(IRCChannel *channel) { if (isPrintOnlyMessage == NO) { switch (channel.type) { case IRCChannelTypeChannel: { /* Rename the user in the channel */ IRCChannelUser *member = [user userAssociatedWithChannel:channel]; if (member == nil) { return; } [channel resortMember:member]; break; } case IRCChannelTypePrivateMessage: { /* Rename private message if one with old name is found */ if ([oldNickname isEqualToStringIgnoringCase:channel.name] == NO) { return; } IRCChannel *newNicknameQuery = [self findChannel:newNickname]; if (newNicknameQuery) { break; } channel.name = newNickname; [mainWindow() reloadTreeItem:channel]; [mainWindow() updateTitleFor:channel]; // Refresh hostmask break; } default: { return; } } } // isPrintOnlyMessage == NO /* Determine whether the message should be printed */ if (channel.isChannel) { BOOL printMessage = [self postReceivedMessage:m withText:newNickname destinedFor:channel]; if (printMessage && myself == NO) { if ([TPCPreferences showJoinLeave] == NO) { printMessage = NO; } else if (channel.config.ignoreGeneralEventMessages) { printMessage = NO; } else if (oldNicknameIgnoreInfo) { printMessage = (oldNicknameIgnoreInfo.ignoreGeneralEventMessages == NO); } } if (printMessage == NO) { return; } } /* Print message */ [self print:messageToPrint by:nil inChannel:channel asType:TVCLogLineTypeNick command:m.command receivedAt:m.receivedAt]; }; /* Target print */ if (isPrintOnlyMessage) { IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { return; } printingBlock(channel); return; } /* Continue with normal operations */ [self renameUser:user to:newNickname]; for (IRCChannel *c in self.channelList) { printingBlock(c); } } - (void)receiveMode:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] > 1); BOOL isPrintOnlyMessage = m.isPrintOnlyMessage; NSString *sender = m.senderNickname; NSString *channelName = [m paramAt:0]; NSString *modeString = [m sequence:1]; /* Present user modes */ if ([self stringIsChannelName:channelName] == NO) { BOOL printMessage = [self postReceivedCommand:@"UMODE" withText:modeString destinedFor:nil referenceMessage:m]; if (printMessage) { [self print:TXTLS(@"IRC[v5d-ix]", sender, modeString) by:nil inChannel:nil asType:TVCLogLineTypeMode command:m.command receivedAt:m.receivedAt]; } return; } /* Present channel modes */ IRCChannel *channel = [self findChannel:channelName]; if (channel == nil || channel.isChannel == NO) { return; } if (isPrintOnlyMessage == NO) { NSArray *modes = [channel.modeInfo updateModes:modeString]; for (IRCModeInfo *mode in modes) { if ([mode isModeForChangingMemberModeOn:self] == NO) { continue; } [channel changeMember:mode.modeParameter mode:mode.modeSymbol value:mode.modeIsSet]; } } BOOL printMessage = [self postReceivedMessage:m withText:modeString destinedFor:channel]; if (printMessage) { printMessage = ([TPCPreferences showJoinLeave] && channel.config.ignoreGeneralEventMessages == NO); } if (printMessage) { [self print:TXTLS(@"IRC[v5d-ix]", sender, modeString) by:nil inChannel:channel asType:TVCLogLineTypeMode command:m.command receivedAt:m.receivedAt]; } if (isPrintOnlyMessage == NO) { [mainWindow() updateTitleFor:channel]; } } - (void)receiveTopic:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] == 2); BOOL isPrintOnlyMessage = m.isPrintOnlyMessage; NSString *sender = m.senderNickname; NSString *channelName = [m paramAt:0]; NSString *topic = [m paramAt:1]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil || channel.isChannel == NO) { return; } if (isPrintOnlyMessage == NO) { channel.topic = topic; } BOOL printMessage = [self postReceivedMessage:m withText:topic destinedFor:channel]; if (printMessage) { [self print:TXTLS(@"IRC[qq2-66]", sender, topic) by:nil inChannel:channel asType:TVCLogLineTypeTopic command:m.command receivedAt:m.receivedAt]; } } - (void)receiveInvite:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] == 2); NSString *sender = m.senderNickname; NSString *channelName = [m paramAt:1]; NSString *message = TXTLS(@"IRC[qw4-t3]", sender, m.senderUsername, m.senderAddress, channelName); /* Invite notifications are sent to frontmost channel on server of if it is not on server, then it will be redirected to console. */ BOOL printMessage = [self postReceivedMessage:m withText:channelName destinedFor:nil]; if (printMessage) { [self print:message by:nil inChannel:[mainWindow() selectedChannelOn:self] asType:TVCLogLineTypeInvite command:m.command receivedAt:m.receivedAt]; } [self notifyEvent:TXNotificationTypeInvite lineType:TVCLogLineTypeInvite target:nil nickname:sender text:channelName]; if ([TPCPreferences autoJoinOnInvite]) { [self joinUnlistedChannel:channelName]; } } - (void)receiveError:(IRCMessage *)m { NSParameterAssert(m != nil); NSString *message = m.sequence; if (([message hasPrefix:@"Closing Link:"] && [message hasSuffix:@"(Excess Flood)"]) || ([message hasPrefix:@"Closing Link:"] && [message hasSuffix:@"(Max SendQ exceeded)"])) { __weak IRCClient *weakSelf = self; self.disconnectCallback = ^{ [weakSelf cancelReconnect]; }; } [self printError:message asCommand:m.command]; } - (void)receiveCertInfo:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] == 2); /* CERTINFO is not a standard command for Textual to receive which means we should be strict about what conditions we will accept it under. */ if (self.zncBouncerIsSendingCertificateInfo == NO || m.senderIsServer == NO || [m.senderNickname isEqualToString:@"znc.in"] == NO) { return; } /* The data we expect to receive should be chunk split which means it is safe to assume a maximum length. */ NSString *data = m.sequence; if (data.length < 2 || data.length > 65) { return; } /* Write line to the mutable buffer */ if ( self.zncBouncerCertificateChainDataMutable) { [self.zncBouncerCertificateChainDataMutable appendFormat:@"%@\n", data]; } } - (void)receiveBatch:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] >= 1); NSString *batchToken = [m paramAt:0]; if (batchToken.length <= 1) { LogToConsoleError("Cannot process BATCH command because [batchToken length] <= 1"); return; } NSString *batchType = [m paramAt:1]; BOOL isBatchOpening = NO; if ([batchToken hasPrefix:@"+"]) { batchToken = [batchToken substringFromIndex:1]; isBatchOpening = YES; } else if ([batchToken hasPrefix:@"-"]) { batchToken = [batchToken substringFromIndex:1]; isBatchOpening = NO; } else { LogToConsoleError("Cannot process BATCH command because there was no open or close modifier"); return; } if ([batchToken onlyContainsCharactersFromCharacterSet:[NSCharacterSet Ato9UnderscoreDash]] == NO) { LogToConsoleError("Cannot process BATCH command because the batch token contains illegal characters"); return; } if (isBatchOpening == NO) { /* Find batch message matching known token */ IRCMessageBatchMessage *thisBatchMessage = [self.batchMessages queuedEntryWithBatchToken:batchToken]; if (thisBatchMessage == nil) { LogToConsoleError("Cannot process BATCH command because -queuedEntryWithBatchToken: returned nil"); return; } thisBatchMessage.batchIsOpen = NO; /* If this batch message has a parent batch, then we do not remove this batch or process it until the close statement for the parent is received. */ if (thisBatchMessage.parentBatchMessage) { return; // Nothing left to do... } batchType = thisBatchMessage.batchType; /* Process queued entries for this batch message. */ /* The method used for processing queued entries will also remove it from queue once completed. */ [self recursivelyProcessBatchMessage:thisBatchMessage]; /* Set vendor specific flags based on BATCH command values */ if ([batchType isEqualToString:@"znc.in/playback"]) { self.zncBouncerIsPlayingBackHistory = NO; } else if ([batchType isEqualToString:@"znc.in/tlsinfo"]) { self.zncBouncerIsSendingCertificateInfo = NO; } } else // isBatchOpening == NO { /* Check batch= value to look for possible parent batch.*/ IRCMessageBatchMessage *parentBatchMessage = nil; NSString *parentBatchMessageToken = m.batchToken; if (parentBatchMessageToken) { parentBatchMessage = [self.batchMessages queuedEntryWithBatchToken:parentBatchMessageToken]; } /* Create new batch message and queue it. */ IRCMessageBatchMessage *newBatchMessage = [IRCMessageBatchMessage new]; newBatchMessage.batchIsOpen = YES; newBatchMessage.batchToken = batchToken; newBatchMessage.batchType = batchType; newBatchMessage.parentBatchMessage = parentBatchMessage; [self.batchMessages queueEntry:newBatchMessage]; /* Set vendor specific flags based on BATCH command values */ if ([batchType isEqualToString:@"znc.in/playback"]) { self.zncBouncerIsPlayingBackHistory = self.isConnectedToZNC; } else if ([batchType isEqualToString:@"znc.in/tlsinfo"]) { self.zncBouncerIsSendingCertificateInfo = self.isConnectedToZNC; /* If this is parent batch (there is no @batch=), then we reset the mutable object to read new data. */ if (parentBatchMessageToken == nil) { self.zncBouncerCertificateChainDataMutable = [NSMutableString string]; } } } } - (void)receiveChangeHost:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] == 2); NSString *username = [m paramAt:0]; if ([username isHostmaskUsernameOn:self] == NO) { LogToConsoleError("Username ('%{private}@') received from CHGHOST command is improperly formatted", username); return; } NSString *address = [m paramAt:1]; if ([address isHostmaskAddressOn:self] == NO) { LogToConsoleError("Address ('%{private}@') received from CHGHOST command is improperly formatted", address); return; } NSString *nickname = m.senderNickname; [self modifyUserUserWithNickname:nickname withBlock:^(IRCUserMutable *userMutable) { userMutable.username = username; userMutable.address = address; }]; } #pragma mark - #pragma mark BATCH Command - (id)queuedBatchMessageWithToken:(NSString *)batchToken { return [self.batchMessages queuedEntryWithBatchToken:batchToken]; } - (BOOL)filterBatchCommandIncomingData:(IRCMessage *)m { NSParameterAssert(m != nil); NSString *batchToken = m.batchToken; if (batchToken) { IRCMessageBatchMessage *thisBatchMessage = [self.batchMessages queuedEntryWithBatchToken:batchToken]; if (thisBatchMessage.batchIsOpen) { [thisBatchMessage queueEntry:m]; return YES; } } return NO; } - (void)recursivelyProcessBatchMessage:(IRCMessageBatchMessage *)batchMessage { [self recursivelyProcessBatchMessage:batchMessage depth:0]; } - (void)recursivelyProcessBatchMessage:(IRCMessageBatchMessage *)batchMessage depth:(NSInteger)recursionDepth { NSParameterAssert(batchMessage != nil); if (batchMessage.batchIsOpen) { return; } NSArray *queuedEntries = batchMessage.queuedEntries; for (id queuedEntry in queuedEntries) { if ([queuedEntry isKindOfClass:[IRCMessage class]]) { [self processIncomingMessage:queuedEntry]; } else if ([queuedEntry isKindOfClass:[IRCMessageBatchMessage class]]) { [self recursivelyProcessBatchMessage:queuedEntry depth:(recursionDepth + 1)]; } } if (recursionDepth == 0) { [self.batchMessages dequeueEntry:batchMessage]; } } #pragma mark - #pragma mark Server Capability - (void)enableCapability:(ClientIRCv3SupportedCapability)capability { if ([self isCapabilityEnabled:capability] == NO) { self->_capabilities |= capability; } } - (void)disableCapability:(ClientIRCv3SupportedCapability)capability { if ([self isCapabilityEnabled:capability]) { self->_capabilities &= ~capability; } } - (BOOL)isCapabilityEnabled:(ClientIRCv3SupportedCapability)capability { return ((self->_capabilities & capability) == capability); } - (void)enablePendingCapability:(ClientIRCv3SupportedCapability)capability { @synchronized (self.capabilitiesPending) { [self.capabilitiesPending addObjectWithoutDuplication:@(capability)]; } } - (void)disablePendingCapability:(ClientIRCv3SupportedCapability)capability { @synchronized (self.capabilitiesPending) { [self.capabilitiesPending removeObject:@(capability)]; } } - (BOOL)isPendingCapabilityEnabled:(ClientIRCv3SupportedCapability)capability { @synchronized (self.capabilitiesPending) { return [self.capabilitiesPending containsObject:@(capability)]; } } - (nullable NSString *)capabilityStringValue:(ClientIRCv3SupportedCapability)capability { NSString *stringValue = nil; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wswitch" switch (capability) { case ClientIRCv3SupportedCapabilityAwayNotify: { stringValue = @"away-notify"; break; } case ClientIRCv3SupportedCapabilityBatch: { stringValue = @"batch"; break; } case ClientIRCv3SupportedCapabilityChangeHost: { stringValue = @"chghost"; break; } case ClientIRCv3SupportedCapabilityEchoMessage: { stringValue = @"echo-message"; break; } case ClientIRCv3SupportedCapabilityIdentifyCTCP: { stringValue = @"identify-ctcp"; break; } case ClientIRCv3SupportedCapabilityIdentifyMsg: { stringValue = @"identify-msg"; break; } case ClientIRCv3SupportedCapabilityMultiPrefix: { stringValue = @"multi-prefix"; break; } case ClientIRCv3SupportedCapabilityPlayback: { stringValue = @"playback"; break; } case ClientIRCv3SupportedCapabilitySASLExternal: case ClientIRCv3SupportedCapabilitySASLPlainText: case ClientIRCv3SupportedCapabilitySASLGeneric: case ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL: case ClientIRCv3SupportedCapabilityIsInSASLNegotiation: { stringValue = @"sasl"; break; } case ClientIRCv3SupportedCapabilityServerTime: { stringValue = @"server-time"; break; } case ClientIRCv3SupportedCapabilityUserhostInNames: { stringValue = @"userhost-in-names"; break; } case ClientIRCv3SupportedCapabilityMonitorCommand: { stringValue = @"monitor-command"; break; } case ClientIRCv3SupportedCapabilityWatchCommand: { stringValue = @"watch-command"; break; } case ClientIRCv3SupportedCapabilityPlanioPlayback: { stringValue = @"plan.io/playback"; break; } case ClientIRCv3SupportedCapabilityZNCCertInfoModule: { stringValue = @"znc.in/tlsinfo"; break; } case ClientIRCv3SupportedCapabilityZNCPlaybackModule: { stringValue = @"znc.in/playback"; break; } case ClientIRCv3SupportedCapabilityZNCSelfMessage: { stringValue = @"znc.in/self-message"; break; } case ClientIRCv3SupportedCapabilityZNCServerTime: { stringValue = @"znc.in/server-time"; break; } case ClientIRCv3SupportedCapabilityZNCServerTimeISO: { stringValue = @"znc.in/server-time-iso"; break; } } #pragma clang diagnostic pop return stringValue; } - (ClientIRCv3SupportedCapability)capabilityFromStringValue:(NSString *)capabilityString { NSParameterAssert(capabilityString != nil); if ([capabilityString isEqualToStringIgnoringCase:@"away-notify"]) { return ClientIRCv3SupportedCapabilityAwayNotify; } else if ([capabilityString isEqualToStringIgnoringCase:@"batch"]) { return ClientIRCv3SupportedCapabilityBatch; } else if ([capabilityString isEqualToStringIgnoringCase:@"chghost"]) { return ClientIRCv3SupportedCapabilityChangeHost; } else if ([capabilityString isEqualToStringIgnoringCase:@"echo-message"]) { return ClientIRCv3SupportedCapabilityEchoMessage; } else if ([capabilityString isEqualToStringIgnoringCase:@"multi-prefix"]) { return ClientIRCv3SupportedCapabilityMultiPrefix; } else if ([capabilityString isEqualToStringIgnoringCase:@"identify-msg"]) { return ClientIRCv3SupportedCapabilityIdentifyMsg; } else if ([capabilityString isEqualToStringIgnoringCase:@"identify-ctcp"]) { return ClientIRCv3SupportedCapabilityIdentifyCTCP; } else if ([capabilityString isEqualToStringIgnoringCase:@"sasl"]) { return ClientIRCv3SupportedCapabilitySASLGeneric; } else if ([capabilityString isEqualToStringIgnoringCase:@"server-time"]) { return ClientIRCv3SupportedCapabilityServerTime; } else if ([capabilityString isEqualToStringIgnoringCase:@"userhost-in-names"]) { return ClientIRCv3SupportedCapabilityUserhostInNames; } else if ([capabilityString isEqualToStringIgnoringCase:@"plan.io/playback"]) { return ClientIRCv3SupportedCapabilityPlanioPlayback; } else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/playback"]) { return ClientIRCv3SupportedCapabilityZNCPlaybackModule; } else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/self-message"]) { return ClientIRCv3SupportedCapabilityZNCSelfMessage; } else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/server-time"]) { return ClientIRCv3SupportedCapabilityZNCServerTime; } else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/server-time-iso"]) { return ClientIRCv3SupportedCapabilityZNCServerTimeISO; } else if ([capabilityString isEqualToStringIgnoringCase:@"znc.in/tlsinfo"]) { return ClientIRCv3SupportedCapabilityZNCCertInfoModule; } return 0; } - (NSString *)enabledCapabilitiesStringValue { NSMutableArray *enabledCapabilities = [NSMutableArray array]; void (^appendValue)(ClientIRCv3SupportedCapability) = ^(ClientIRCv3SupportedCapability capability) { if ([self isCapabilityEnabled:capability] == NO) { return; } NSString *stringValue = [self capabilityStringValue:capability]; if (stringValue) { [enabledCapabilities addObject:stringValue]; } }; appendValue(ClientIRCv3SupportedCapabilityAwayNotify); appendValue(ClientIRCv3SupportedCapabilityBatch); appendValue(ClientIRCv3SupportedCapabilityChangeHost); appendValue(ClientIRCv3SupportedCapabilityEchoMessage); appendValue(ClientIRCv3SupportedCapabilityIdentifyCTCP); appendValue(ClientIRCv3SupportedCapabilityIdentifyMsg); appendValue(ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL); appendValue(ClientIRCv3SupportedCapabilityMultiPrefix); appendValue(ClientIRCv3SupportedCapabilityPlayback); appendValue(ClientIRCv3SupportedCapabilityServerTime); appendValue(ClientIRCv3SupportedCapabilityUserhostInNames); appendValue(ClientIRCv3SupportedCapabilityZNCCertInfoModule); appendValue(ClientIRCv3SupportedCapabilityZNCPlaybackModule); appendValue(ClientIRCv3SupportedCapabilityZNCSelfMessage); NSString *stringValue = [enabledCapabilities componentsJoinedByString:@", "]; return stringValue; } - (void)sendNextCapability { if (self.capabilityNegotiationIsPaused) { return; } @synchronized (self.capabilitiesPending) { /* -CapabilitiesPending can contain values that are used internally for state traking and should never meet the socket. To workaround this as best we can, we scan the array for the first capability that is acceptable for negotiation. */ NSUInteger nextCapabilityIndex = [self.capabilitiesPending indexOfObjectPassingTest:^BOOL(NSNumber *capabilityPending, NSUInteger index, BOOL *stop) { ClientIRCv3SupportedCapability capability = capabilityPending.unsignedIntegerValue; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wtautological-compare" return (capability == ClientIRCv3SupportedCapabilityAwayNotify || capability == ClientIRCv3SupportedCapabilityBatch || capability == ClientIRCv3SupportedCapabilityChangeHost || capability == ClientIRCv3SupportedCapabilityEchoMessage || capability == ClientIRCv3SupportedCapabilityIdentifyCTCP || capability == ClientIRCv3SupportedCapabilityIdentifyMsg || capability == ClientIRCv3SupportedCapabilityMultiPrefix || capability == ClientIRCv3SupportedCapabilitySASLGeneric || capability == ClientIRCv3SupportedCapabilityServerTime || capability == ClientIRCv3SupportedCapabilityUserhostInNames || capability == ClientIRCv3SupportedCapabilityPlanioPlayback || capability == ClientIRCv3SupportedCapabilityZNCCertInfoModule || capability == ClientIRCv3SupportedCapabilityZNCPlaybackModule || capability == ClientIRCv3SupportedCapabilityZNCSelfMessage || capability == ClientIRCv3SupportedCapabilityZNCServerTime || capability == ClientIRCv3SupportedCapabilityZNCServerTimeISO); #pragma clang diagnostic pop }]; if (nextCapabilityIndex == NSNotFound) { [self sendCapability:@"END" data:nil]; return; } ClientIRCv3SupportedCapability capability = [self.capabilitiesPending unsignedIntegerAtIndex:nextCapabilityIndex]; [self.capabilitiesPending removeObjectAtIndex:nextCapabilityIndex]; NSString *stringValue = [self capabilityStringValue:capability]; [self sendCapability:@"REQ" data:stringValue]; } } - (void)pauseCapabilityNegotiation { self.capabilityNegotiationIsPaused = YES; } - (void)resumeCapabilityNegotiation { self.capabilityNegotiationIsPaused = NO; [self sendNextCapability]; } - (BOOL)isCapabilitySupported:(NSString *)capabilityString { NSParameterAssert(capabilityString != nil); // Information about several of these supported CAP // extensions can be found at: http://ircv3.atheme.org if ([capabilityString isEqualToStringIgnoringCase:@"echo-message"]) { return [TPCPreferences enableEchoMessageCapability]; } return ([capabilityString isEqualToStringIgnoringCase:@"away-notify"] || [capabilityString isEqualToStringIgnoringCase:@"batch"] || [capabilityString isEqualToStringIgnoringCase:@"chghost"] || [capabilityString isEqualToStringIgnoringCase:@"identify-ctcp"] || [capabilityString isEqualToStringIgnoringCase:@"identify-msg"] || [capabilityString isEqualToStringIgnoringCase:@"multi-prefix"] || [capabilityString isEqualToStringIgnoringCase:@"sasl"] || [capabilityString isEqualToStringIgnoringCase:@"server-time"] || [capabilityString isEqualToStringIgnoringCase:@"userhost-in-names"] || [capabilityString isEqualToStringIgnoringCase:@"plan.io/playback"] || [capabilityString isEqualToStringIgnoringCase:@"znc.in/playback"] || [capabilityString isEqualToStringIgnoringCase:@"znc.in/self-message"] || [capabilityString isEqualToStringIgnoringCase:@"znc.in/server-time"] || [capabilityString isEqualToStringIgnoringCase:@"znc.in/server-time-iso"] || [capabilityString isEqualToStringIgnoringCase:@"znc.in/tlsinfo"]); } - (void)toggleCapability:(NSString *)capabilityString enabled:(BOOL)enabled { [self toggleCapability:capabilityString enabled:enabled isUpdateRequest:NO]; } - (void)toggleCapability:(NSString *)capabilityString enabled:(BOOL)enabled isUpdateRequest:(BOOL)isUpdateRequest { NSParameterAssert(capabilityString != nil); if ([capabilityString isEqualToStringIgnoringCase:@"sasl"]) { if (enabled) { if ([self sendSASLIdentificationRequest]) { [self pauseCapabilityNegotiation]; } } return; } ClientIRCv3SupportedCapability capability = [self capabilityFromStringValue:capabilityString]; if (capability == 0) { return; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wtautological-compare" if (capability == ClientIRCv3SupportedCapabilityZNCServerTime || capability == ClientIRCv3SupportedCapabilityZNCServerTimeISO) { capability = ClientIRCv3SupportedCapabilityServerTime; } if (capability == ClientIRCv3SupportedCapabilityPlanioPlayback || capability == ClientIRCv3SupportedCapabilityZNCPlaybackModule) { capability = ClientIRCv3SupportedCapabilityPlayback; } #pragma clang diagnostic pop if (enabled) { [self enableCapability:capability]; } else { [self disableCapability:capability]; } } - (void)processPendingCapability:(NSString *)capabilityString { NSParameterAssert(capabilityString != nil); NSArray *components = [capabilityString componentsSeparatedByString:@"="]; NSString *capability = capabilityString; NSArray *capabilityOptions = nil; if (components.count == 2) { capability = components[0]; capabilityOptions = [components[1] componentsSeparatedByString:@","]; } [self processPendingCapability:capability options:capabilityOptions]; } - (void)processPendingCapability:(NSString *)capabilityString options:(nullable NSArray *)capabilityOptions { NSParameterAssert(capabilityString != nil); if ([self isCapabilitySupported:capabilityString] == NO) { return; } if ([capabilityString isEqualToString:@"sasl"]) { [self processPendingCapabilityForSASL:capabilityOptions]; return; } ClientIRCv3SupportedCapability capability = [self capabilityFromStringValue:capabilityString]; [self enablePendingCapability:capability]; } - (void)receiveCapabilityOrAuthenticationRequest:(IRCMessage *)m { /* Implementation based off Colloquy's own. */ NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] > 0); NSString *command = m.command; NSString *modifier = [m paramAt:0]; NSString *subcommand = [m paramAt:1]; NSString *actions = [m sequence:2]; if ([command isEqualToStringIgnoringCase:@"CAP"]) { if ([subcommand isEqualToStringIgnoringCase:@"LS"]) { NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *cap in caps) { [self processPendingCapability:cap]; } } else if ([subcommand isEqualToStringIgnoringCase:@"ACK"]) { NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *cap in caps) { [self toggleCapability:cap enabled:YES isUpdateRequest:NO]; } } else if ([subcommand isEqualToStringIgnoringCase:@"NAK"]) { NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *cap in caps) { [self toggleCapability:cap enabled:NO isUpdateRequest:NO]; } } else if ([subcommand isEqualToStringIgnoringCase:@"NEW"]) { NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *cap in caps) { [self processPendingCapability:cap]; } } else if ([subcommand isEqualToStringIgnoringCase:@"DEL"]) { NSArray *caps = [actions componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *cap in caps) { [self toggleCapability:cap enabled:NO isUpdateRequest:YES]; } } [self sendNextCapability]; } else if ([command isEqualToStringIgnoringCase:@"AUTHENTICATE"]) { if ([modifier isEqualToString:@"+"]) { [self sendSASLIdentificationInformation]; } } [self postReceivedMessage:m]; } #pragma mark - #pragma mark SASL Negotiation - (void)processPendingCapabilityForSASL:(nullable NSArray *)capabilityOptions { ClientIRCv3SupportedCapability identificationMechanism = 0; if (self.socket.isConnectedWithClientSideCertificate && self.config.saslAuthenticationDisableExternalMechanism == NO) { if (capabilityOptions.count == 0 || [capabilityOptions containsObjectIgnoringCase:@"EXTERNAL"]) { identificationMechanism = ClientIRCv3SupportedCapabilitySASLExternal; [self enablePendingCapability:ClientIRCv3SupportedCapabilitySASLExternal]; } } if (identificationMechanism == 0 && self.config.nicknamePassword.length > 0) { if (capabilityOptions.count == 0 || [capabilityOptions containsObjectIgnoringCase:@"PLAIN"]) { identificationMechanism = ClientIRCv3SupportedCapabilitySASLPlainText; [self enablePendingCapability:ClientIRCv3SupportedCapabilitySASLPlainText]; } } if (identificationMechanism != 0) { [self enablePendingCapability:ClientIRCv3SupportedCapabilitySASLGeneric]; } } - (void)sendSASLIdentificationInformation { if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilityIsInSASLNegotiation] == NO) { return; } if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilitySASLPlainText]) { NSString *authString = [NSString stringWithFormat:@"%@%C%@%C%@", self.config.username, 0x00, self.config.username, 0x00, self.config.nicknamePassword]; NSArray *authStrings = [authString base64EncodingWithLineLength:400]; for (NSString *string in authStrings) { [self sendCapabilityAuthenticate:string]; } if (authStrings.count == 0 || ((NSString *)authStrings.lastObject).length == 400) { [self sendCapabilityAuthenticate:@"+"]; } } else if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilitySASLExternal]) { [self sendCapabilityAuthenticate:@"+"]; } } - (BOOL)sendSASLIdentificationRequest { if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL]) { return NO; } if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilityIsInSASLNegotiation]) { return NO; } [self enablePendingCapability:ClientIRCv3SupportedCapabilityIsInSASLNegotiation]; if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilitySASLPlainText]) { [self sendCapabilityAuthenticate:@"PLAIN"]; return YES; } else if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilitySASLExternal]) { [self sendCapabilityAuthenticate:@"EXTERNAL"]; return YES; } return NO; } - (void)resetSASLNegotiation { [self disablePendingCapability:ClientIRCv3SupportedCapabilitySASLGeneric]; [self disablePendingCapability:ClientIRCv3SupportedCapabilitySASLPlainText]; [self disablePendingCapability:ClientIRCv3SupportedCapabilitySASLExternal]; [self disablePendingCapability:ClientIRCv3SupportedCapabilityIsInSASLNegotiation]; [self disableCapability:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL]; } #pragma mark - #pragma mark Protocol Handlers - (void)receivePing:(IRCMessage *)m { NSParameterAssert(m != nil); NSAssertReturn([m paramsCount] > 0); NSString *token = [m sequence:0]; [self sendPong:token]; [self postReceivedMessage:m]; } - (void)receiveAwayNotifyCapability:(IRCMessage *)m { NSParameterAssert(m != nil); if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityAwayNotify] == NO) { return; } BOOL away = (m.sequence.length > 0); NSString *nickname = m.senderNickname; [self modifyUserWithNickname:nickname asAway:away]; } - (void)receiveInit:(IRCMessage *)m // Raw numeric = 001 { NSParameterAssert(m != nil); /* Manage timers */ [self startPongTimer]; [self stopRetryTimer]; /* Manage properties */ self.isLoggedIn = YES; self.supportInfo.serverAddress = m.senderHostmask; self.invokingISONCommandForFirstTime = YES; self.reconnectEnabledBecauseOfSleepMode = NO; self.tryingNicknameSentNickname = nil; self.userNickname = [m paramAt:0]; self.successfulConnects += 1; /* Begin enforcing flood control */ [self.socket enforceFloodControl]; /* Post event */ [self postEventToViewController:@"serverConnected"]; [self notifyEvent:TXNotificationTypeConnect lineType:TVCLogLineTypeDebug]; /* Perform login commands */ for (__strong NSString *command in self.config.loginCommands) { if ([command hasPrefix:@"/"]) { command = [command substringFromIndex:1]; } [self sendCommand:command completeTarget:NO target:nil]; } /* Request certificate information */ if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityZNCCertInfoModule]) { [self sendCommand:@"send-data" toZNCModuleNamed:@"tlsinfo"]; } /* Request playback since the last seen message when previously connected */ [self requestPlayback]; /* Activate existing queries */ for (IRCChannel *c in self.channelList) { if (c.privateMessage) { [c activate]; [mainWindow() reloadTreeItem:c]; } } [mainWindow() reloadTreeItem:self]; [mainWindow() updateTitleFor:self]; [mainWindowTextField() updateSegmentedController]; /* Everything else */ if (self.config.autojoinWaitsForNickServ == NO || [self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL]) { [self performAutoJoin]; } else { /* If we wait for NickServ we set a timer of 3.0 seconds before performing auto join. When this timer is executed, if we do not have any knowledge of NickServ existing on the current server, then we perform the autojoin. This is primarily a fix for the ZNC SASL module which will complete identification before connecting and once connected Textual will have no knowledge of whether the local user is identified or not. */ /* NickServ will send a notice asking for identification as soon as connection occurs so this is the best patch. At least for right now. */ if (self.isConnectedToZNC) { [self performSelectorInCommonModes:@selector(performAutoJoin) withObject:nil afterDelay:3.0]; } else { [self startAutojoinDelayedWarningTimer]; } } /* We need time for the server to send its configuration */ [self performSelectorInCommonModes:@selector(populateISONTrackedUsersList) withObject:nil afterDelay:10.0]; } - (void)receiveNumericReply:(IRCMessage *)m { NSParameterAssert(m != nil); NSInteger numeric = m.commandNumeric; if (numeric > 400 && numeric < 597 && numeric != ERR_NOMOTD) { [self receiveErrorNumericReply:m]; return; } BOOL printMessage = YES; /* These numerics are treated differently below which is why the exception exists. For example, for channel topic, only the contents of the topic are sent to the filter. */ if (numeric != RPL_UMODEIS && numeric != RPL_CHANNELMODEIS && numeric != RPL_TOPIC && numeric != RPL_TOPICWHOTIME) { printMessage = [self postReceivedMessage:m]; } switch (numeric) { case RPL_WELCOME: { [self receiveInit:m]; if (printMessage) { [self printReply:m]; } break; } case RPL_YOURHOST: case RPL_CREATED: case RPL_MYINFO: { if (printMessage) { [self printReply:m]; } break; } case RPL_ISUPPORT: { NSAssertReturn([m paramsCount] >= 3); NSMutableArray *params = [m.params mutableCopy]; [params removeObjectAtIndex:0]; // Remove nickname NSString *message = params.lastObject; [params removeLastObject]; // Remove "are supported by this server" NSString *configuration = [params componentsJoinedByString:@" "]; [self.supportInfo processConfigurationData:configuration]; if (printMessage) { NSString *configurationFormatted = self.supportInfo.stringValueForLastUpdate; [self printDebugInformationToConsole:TXTLS(@"IRC[u51-nn]", configurationFormatted, message) asCommand:m.command]; } break; } case RPL_REDIR: { NSAssertReturn([m paramsCount] == 4); NSString *serverAddress = [m paramAt:1]; NSString *serverPort = [m paramAt:2]; self.disconnectType = IRCClientDisconnectModeServerRedirect; /* If the address is thought to be invalid, then we still perform the disconnect suggested by the redirect, but we do not go any further than that. */ if (serverAddress.validInternetAddress == NO || serverPort.validInternetPort == NO) { [self disconnect]; return; } /* Perform reconnect to specified locations */ __weak IRCClient *weakSelf = self; self.disconnectCallback = ^{ [weakSelf connect]; }; [self disconnect]; /* -disconnect would destroy this so we set them after... */ self.temporaryServerAddressOverride = serverAddress; self.temporaryServerPortOverride = serverPort.integerValue; break; } case RPL_STATSCONN: case RPL_LUSERCLIENT: case RPL_LUSERHOP: case RPL_LUSERUNKNOWN: case RPL_LUSERCHANNELS: case RPL_LUSERME: { if (printMessage) { [self printReply:m]; } break; } case RPL_LOCALUSERS: case RPL_GLOBALUSERS: { NSAssertReturn(printMessage); NSString *message = nil; if (m.paramsCount == 4) { /* Removes user count from in front of messages on IRCds that send them. Example: ">> :irc.example.com 265 Guest 2 3 :Current local users 2, max 3" */ message = [m sequence:3]; } else { message = m.sequence; } [self print:message by:nil inChannel:nil asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; break; } case RPL_MOTD: case RPL_MOTDSTART: case RPL_ENDOFMOTD: case ERR_NOMOTD: { NSAssertReturn(printMessage); if ([TPCPreferences displayServerMOTD] == NO) { break; } if (numeric == ERR_NOMOTD) { [self printErrorReply:m]; } else { [self printReply:m]; } break; } case RPL_UMODEIS: { NSAssertReturn([m paramsCount] > 1); NSString *nickname = [m paramAt:0]; NSString *modeString = [m paramAt:1]; if ([modeString isEqualToString:@"+"]) { break; } printMessage = [self postReceivedMessage:m withText:modeString destinedFor:nil]; if (printMessage) { [self print:TXTLS(@"IRC[ipj-34]", nickname, modeString) by:nil inChannel:nil asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; } break; } case RPL_AWAY: { NSAssertReturn([m paramsCount] == 3); NSString *awayNickname = [m paramAt:1]; NSString *awayComment = [m paramAt:2]; IRCChannel *channel = [self findChannel:awayNickname]; NSString *message = TXTLS(@"IRC[c1h-fq]", awayNickname, awayComment); if (channel == nil) { channel = [mainWindow() selectedChannelOn:self]; } IRCUser *user = [self findUser:awayNickname]; if ( user) { if (self.monitorAwayStatus) { [user markAsAway]; } if (user.presentAwayMessageFor301 == NO) { break; } } if (printMessage) { [self print:message by:nil inChannel:channel asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; } break; } case RPL_UNAWAY: case RPL_NOWAWAY: { BOOL away = (numeric == RPL_NOWAWAY); self.userIsAway = away; [mainWindow() updateTitle]; if (printMessage) { [self printReply:m]; } /* Update our own status. This has to only be done with away-notify CAP enabled. Old, WHO based information requests will still show our own status. */ IRCUser *myself = self.myself; if (myself == nil) { break; } [self modifyUser:myself asAway:away]; break; } case RPL_CHANNELSMSG: case RPL_WHOISBOT: case RPL_WHOISHELPOP: case RPL_WHOISHOST: case RPL_WHOISMODES: case RPL_WHOISOPERATOR: case RPL_WHOISREALIP: case RPL_WHOISREGNICK: case RPL_WHOISSECURE: case RPL_WHOISSPECIAL: { NSAssertReturn([m paramsCount] > 2); if (printMessage) { [self printReply:m inChannel:[mainWindow() selectedChannelOn:self]]; } break; } case RPL_WHOISACTUALLY: { NSAssertReturn([m paramsCount] == 5); NSAssertReturn(printMessage); NSString *nickname = [m paramAt:1]; NSString *hostmask = [m paramAt:2]; NSString *ipAddress = [m paramAt:3]; NSString *message = nil; if (self.inWhowasResponse) { // bahamut sends RPL_WHOISACTUALLY in WHOWAS message = TXTLS(@"IRC[x69-rz]", nickname, hostmask, ipAddress); } else { message = TXTLS(@"IRC[3oa-mv]", nickname, hostmask, ipAddress); } [self print:message by:nil inChannel:[mainWindow() selectedChannelOn:self] asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; break; } case RPL_WHOISUSER: case RPL_WHOWASUSER: { NSAssertReturn([m paramsCount] >= 6); NSString *nickname = [m paramAt:1]; NSString *username = [m paramAt:2]; NSString *address = [m paramAt:3]; NSString *realName = [m paramAt:5]; if ([realName hasPrefix:@":"]) { realName = [realName substringFromIndex:1]; } self.inWhoisResponse = (numeric == RPL_WHOISUSER); self.inWhowasResponse = (numeric == RPL_WHOWASUSER); NSString *message = nil; if (self.inWhowasResponse) { if (printMessage) { message = TXTLS(@"IRC[32c-87]", nickname, username, address, realName); } } else { if (printMessage) { message = TXTLS(@"IRC[plg-lr]", nickname, username, address, realName); } /* Update local cache of our hostmask */ if ([self nicknameIsMyself:nickname]) { NSString *hostmask = [NSString stringWithFormat:@"%@!%@@%@", nickname, username, address]; self.userHostmask = hostmask; } } if (message) { [self print:message by:nil inChannel:[mainWindow() selectedChannelOn:self] asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; } break; } case RPL_WHOISSERVER: { NSAssertReturn([m paramsCount] == 4); NSAssertReturn(printMessage); NSString *nickname = [m paramAt:1]; NSString *serverAddress = [m paramAt:2]; NSString *serverInfo = [m paramAt:3]; NSString *message = nil; if (self.inWhowasResponse) { // bahamut sends RPL_WHOISSERVER in WHOWAS NSString *timeInfo = TXFormatDateLongStyle(serverInfo, YES); if (timeInfo == nil) { timeInfo = serverInfo; } message = TXTLS(@"IRC[cdu-ed]", nickname, serverAddress, timeInfo); } else { message = TXTLS(@"IRC[h19-n2]", nickname, serverAddress, serverInfo); } [self print:message by:nil inChannel:[mainWindow() selectedChannelOn:self] asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; break; } case RPL_WHOISIDLE: { NSAssertReturn([m paramsCount] == 5); NSAssertReturn(printMessage); NSString *nickname = [m paramAt:1]; NSString *idleTime = [m paramAt:2]; NSString *connectTime = [m paramAt:3]; idleTime = TXHumanReadableTimeInterval(idleTime.doubleValue, NO, 0); NSDate *connTimeDate = [NSDate dateWithTimeIntervalSince1970:connectTime.doubleValue]; connectTime = TXFormatDateLongStyle(connTimeDate, YES); NSString *message = TXTLS(@"IRC[6hn-o6]", nickname, connectTime, idleTime); [self print:message by:nil inChannel:[mainWindow() selectedChannelOn:self] asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; break; } case RPL_WHOISCHANNELS: { NSAssertReturn([m paramsCount] == 3); NSAssertReturn(printMessage); NSString *nickname = [m paramAt:1]; NSString *channels = [m paramAt:2]; NSString *message = TXTLS(@"IRC[onk-l5]", nickname, channels); [self print:message by:nil inChannel:[mainWindow() selectedChannelOn:self] asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; break; } case RPL_WHOISACCOUNT: { NSAssertReturn([m paramsCount] == 4); NSAssertReturn(printMessage); NSString *message = [NSString stringWithFormat:@"%@ %@ %@", [m paramAt:1], [m sequence:3], [m paramAt:2]]; [self print:message by:nil inChannel:[mainWindow() selectedChannelOn:self] asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; break; } case RPL_ENDOFWHOIS: { self.inWhoisResponse = NO; /* if (printMessage) { [self printReply:m inChannel:[mainWindow() selectedChannelOn:self]]; } */ break; } case RPL_ENDOFWHOWAS: { self.inWhowasResponse = NO; /* if (printMessage) { [self printReply:m inChannel:[mainWindow() selectedChannelOn:self]]; } */ break; } case RPL_CHANNELMODEIS: { NSAssertReturn([m paramsCount] > 2); NSString *channelName = [m paramAt:1]; NSString *modeString = [m sequence:2]; if ([modeString isEqualToString:@"+"]) { return; } IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { break; } if (channel.isActive) { [channel.modeInfo clear]; [channel.modeInfo updateModes:modeString]; } printMessage = [self postReceivedMessage:m withText:modeString destinedFor:channel]; /* We perform this check after printMessage is defined so that filters have a chance to act on the input. */ /* IRCClient perform mode requests for channels without the user asking for it so we must check that here. */ if (channel.channelModesReceived == NO) { channel.channelModesReceived = YES; } if (printMessage) { NSString *message = channel.modeInfo.stringWithMaskedPassword; [self print:TXTLS(@"IRC[obp-ww]", message) by:nil inChannel:channel asType:TVCLogLineTypeMode command:m.command receivedAt:m.receivedAt]; } break; } case RPL_TOPIC: { NSAssertReturn([m paramsCount] == 3); NSString *channelName = [m paramAt:1]; NSString *topic = [m paramAt:2]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { break; } printMessage = [self postReceivedMessage:m withText:topic destinedFor:channel]; channel.topic = topic; if (printMessage) { [self print:TXTLS(@"IRC[7nm-7v]", topic) by:nil inChannel:channel asType:TVCLogLineTypeTopic command:m.command receivedAt:m.receivedAt]; } break; } case RPL_TOPICWHOTIME: { NSAssertReturn([m paramsCount] == 4); NSString *channelName = [m paramAt:1]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { break; } printMessage = [self postReceivedMessage:m withText:nil destinedFor:channel]; if (printMessage == NO) { return; } NSString *topicSetter = [m paramAt:2]; NSString *setTime = [m paramAt:3]; topicSetter = topicSetter.nicknameFromHostmask; NSDate *setTimeDate = [NSDate dateWithTimeIntervalSince1970:setTime.doubleValue]; setTime = TXFormatDateLongStyle(setTimeDate, YES); NSString *message = TXTLS(@"IRC[y7s-3e]", topicSetter, setTime); [self print:message by:nil inChannel:channel asType:TVCLogLineTypeTopic command:m.command receivedAt:m.receivedAt]; break; } case RPL_CREATIONTIME: { break; // Ignore } case RPL_INVITING: { NSAssertReturn([m paramsCount] == 3); NSAssertReturn(printMessage); NSString *nickname = [m paramAt:1]; NSString *channelName = [m paramAt:2]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { break; } [self print:TXTLS(@"IRC[wk4-rv]", nickname, channelName) by:nil inChannel:channel asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; break; } case RPL_ISON: { /* Present reply to the user if we have destination */ BOOL visibleIsonRequest = self.requestedCommands.visibleIsonRequest; [self.requestedCommands recordIsonRequestClosed]; if (visibleIsonRequest) { if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } /* It is important that we don't process logic for visible requests because if user does ISON for people that aren't on the tracked list and the logic below sees the response missing those, then it will think everyone tracked went offline. */ break; } /* If the ISON records were not requested by the user, then treat the results as user tracking information. */ NSString *onlineNicknamesString = m.sequence; NSArray *onlineNicknames = [onlineNicknamesString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; /* Start going over the list of tracked nicknames */ NSDictionary *trackedUsers = self.trackedUsers.trackedUsers; [trackedUsers enumerateKeysAndObjectsUsingBlock:^(NSString *trackedUser, NSNumber *trackingStatusInt, BOOL *stop) { IRCAddressBookUserTrackingStatus trackingStatus = IRCAddressBookUserTrackingStatusUnknown; /* Was the user on during the last check? */ BOOL ison = trackingStatusInt.boolValue; if (ison) { /* If the user was on before, but is not in the list of ISON users in this reply, then they are considered gone. Log that. */ if ([onlineNicknames containsObjectIgnoringCase:trackedUser] == NO) { if (self.invokingISONCommandForFirstTime == NO) { trackingStatus = IRCAddressBookUserTrackingStatusSignedOff; } } } else { /* If they were not on but now are, then log that too. */ if ([onlineNicknames containsObjectIgnoringCase:trackedUser]) { if (self.invokingISONCommandForFirstTime) { trackingStatus = IRCAddressBookUserTrackingStatusAvailable; } else { trackingStatus = IRCAddressBookUserTrackingStatusSignedOn; } } } /* If something changed (non-nil localization string), then scan the list of address book entries to report the result. */ if (trackingStatus != IRCAddressBookUserTrackingStatusUnknown) { [self statusOfTrackedNickname:trackedUser changedTo:trackingStatus notify:YES]; } }]; // for if (self.invokingISONCommandForFirstTime) { // Reset internal property self.invokingISONCommandForFirstTime = NO; } /* Update private messages */ for (IRCChannel *channel in self.channelList) { if (channel.privateMessage == NO) { continue; } if (channel.isActive) { /* If the user is no longer on, deactivate the private message */ if ([onlineNicknames containsObjectIgnoringCase:channel.name] == NO) { [channel deactivate]; [mainWindow() reloadTreeItem:channel]; } } else { /* Activate the private message if the user is back online */ if ([onlineNicknames containsObjectIgnoringCase:channel.name]) { [channel activate]; [mainWindow() reloadTreeItem:channel]; } } } break; } case RPL_WHOREPLY: { NSAssertReturn([m paramsCount] > 6); /* Present reply to the user if we have destination */ if (self.requestedCommands.visibleWhoRequest) { if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } /* We could remove this and be fine, but it's a lot over overhead to process WHO responses so let's just wait until the next automated one. */ break; } /* Process reply */ NSString *channelName = [m paramAt:1]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { break; } /* Example incoming data: [*][@|+] #freenode znc unaffiliated/namikaze kornbluth.freenode.net Namikaze G 0 Christian #freenode ~D unaffiliated/solprefixer kornbluth.freenode.net solprefixer H 0 solprefixer */ NSString *nickname = [m paramAt:5]; NSString *username = [m paramAt:2]; NSString *address = [m paramAt:3]; NSString *flags = [m paramAt:6]; NSString *realName = [m paramAt:7]; BOOL isAway = NO; BOOL isIRCop = NO; // Field Syntax: [*][@|+] // Strip G or H (away status). NSMutableString *userModes = [NSMutableString string]; for (NSUInteger i = 0; i < flags.length; i++) { NSString *character = [flags stringCharacterAtIndex:i]; if ([character isEqualToString:@"G"]) { isAway = self.monitorAwayStatus; continue; } else if ([character isEqualToString:@"*"]) { isIRCop = YES; continue; } NSString *modeSymbol = [self.supportInfo modeSymbolForUserPrefix:character]; if (modeSymbol == nil) { continue; } [userModes appendString:modeSymbol]; } /* Parameter 7 includes the hop count and real name because it begins with a : Therefore, we cut after the first space to get the real, real name value. */ NSInteger realNameFirstSpace = [realName stringPosition:@" "]; if (realNameFirstSpace > 0 && realNameFirstSpace < realName.length) { realName = [realName substringAfterIndex:realNameFirstSpace]; } /* Find global user and create mutable copy */ IRCUser *user = [self findUser:nickname]; IRCUserMutable *userMutable = nil; if (user == nil) { userMutable = [[IRCUserMutable alloc] initWithNickname:nickname onClient:self]; } else { userMutable = [user mutableCopy]; } userMutable.nickname = nickname; userMutable.username = username; userMutable.address = address; userMutable.isAway = isAway; userMutable.isIRCop = isIRCop; userMutable.realName = realName; /* Insert the user into the client and return the final copy that was */ BOOL userChanged = (user != nil && [user isEqual:userMutable] == NO); IRCUser *userAdded = nil; if (user == nil || userChanged) { userAdded = [self addUserAndReturn:userMutable]; } else { userAdded = user; } /* Find the user associated with this channel */ IRCChannelUser *member = [user userAssociatedWithChannel:channel]; if (member == nil) { IRCChannelUserMutable *memberMutable = [[IRCChannelUserMutable alloc] initWithUser:userAdded]; memberMutable.modes = userModes; [channel addMember:memberMutable]; } else if (userChanged) { /* Determine whether the users were modified in such a way that they require their cell in the user list be resorted. */ /* We do not want to resort unless absolutely necessary because sorting a channel with a few hundred users has overhead. */ BOOL IRCopStatusChanged = (user.isIRCop != userAdded.isIRCop); BOOL resortMember = IRCopStatusChanged; BOOL replaceInAllChannels = (IRCopStatusChanged && [TPCPreferences memberListSortFavorsServerStaff]); if (resortMember) { [channel replaceMember:member withMember:member resort:resortMember replaceInAllChannels:replaceInAllChannels]; } else if (user.isAway != userAdded.isAway) { [mainWindow() updateDrawingForUserInUserList:userAdded]; } } /* Update local cache of our hostmask */ if ([self nicknameIsMyself:nickname]) { NSString *hostmask = [NSString stringWithFormat:@"%@!%@@%@", nickname, username, address]; self.userHostmask = hostmask; } break; } case RPL_ENDOFWHO: { BOOL visibleWhoRequest = self.requestedCommands.visibleWhoRequest; [self.requestedCommands recordWhoRequestClosed]; if (visibleWhoRequest && printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } break; } case RPL_NAMEREPLY: { NSAssertReturn([m paramsCount] > 3); /* Present reply to the user if we have destination */ if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } /* Process reply */ NSString *channelName = [m paramAt:2]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil || channel.channelNamesReceived) { break; } NSString *nicknamesString = [m paramAt:3]; NSArray *nicknames = [nicknamesString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *nickname in nicknames) { if (nickname.length == 0) { continue; } /* Find first character that is not a user mode */ NSMutableString *memberModes = [NSMutableString string]; NSUInteger characterIndex = 0; for (characterIndex = 0; characterIndex < nickname.length; characterIndex++) { NSString *prefix = [nickname stringCharacterAtIndex:characterIndex]; NSString *modeSymbol = [self.supportInfo modeSymbolForUserPrefix:prefix]; if (modeSymbol == nil) { break; } [memberModes appendString:modeSymbol]; } // for /* Split away hostmask if available */ NSString *newNickname = [nickname substringFromIndex:characterIndex]; NSString *nicknameInt = nil; NSString *usernameInt = nil; NSString *addressInt = nil; if ([newNickname hostmaskComponents:&nicknameInt username:&usernameInt address:&addressInt onClient:self] == NO) { /* When NAMES reply is not a host, then set the nicknameInt to the value of nickname and leave the rest as nil. */ nicknameInt = newNickname; } /* Find global user */ /* An instance of IRCUser may already exist from a NAMES reply for another channel. If one already exist, then we don't make an effort to change it's credentials. */ IRCUser *userAdded = nil; IRCUser *user = [self findUser:nicknameInt]; if (user == nil) { IRCUserMutable *userMutable = [[IRCUserMutable alloc] initWithNickname:nicknameInt onClient:self]; userMutable.nickname = nicknameInt; userMutable.username = usernameInt; userMutable.address = addressInt; userAdded = [self addUserAndReturn:userMutable]; } else { userAdded = user; } /* Find channel user */ IRCChannelUser *member = [userAdded userAssociatedWithChannel:channel]; IRCChannelUserMutable *memberMutable = nil; if (member == nil) { memberMutable = [[IRCChannelUserMutable alloc] initWithUser:userAdded]; } else if ([self nicknameIsMyself:nicknameInt]) { memberMutable = [member mutableCopy]; } else { /* If a user with this name already exists in the channel, then we do not continue unless its us. We are added to the channel when the JOIN is received, but we still need modes. */ return; } /* Create channel user */ memberMutable.modes = memberModes; /* Add user to channel */ [channel addMember:memberMutable checkForDuplicates:YES]; } // for break; } case RPL_ENDOFNAMES: { NSAssertReturn([m paramsCount] == 3); /* Present reply to the user if we have destination */ if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } /* Process reply */ NSString *channelName = [m paramAt:1]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil || channel.channelNamesReceived) { break; } channel.channelNamesReceived = YES; /* We have to wait until names are processed before populating defaults for a channel so that we are certain there is actually only one user, which is us. */ if (channel.numberOfMembers == 1 && self.isBrokenIRCd_aka_Twitch == NO) { NSString *defaultModes = channel.config.defaultModes; if (defaultModes.length > 0) { [self sendModes:defaultModes withParametersString:nil inChannel:channel]; } NSString *defaultTopic = channel.config.defaultTopic; if (defaultTopic.length > 0) { [self sendTopicTo:defaultTopic inChannel:channel]; } } /* Update user count in title */ [mainWindow() updateTitleFor:channel]; break; } case RPL_LISTSTART: { TDCServerChannelListDialog *channelListDialog = [self channelListDialog]; if (channelListDialog) { channelListDialog.contentAlreadyReceived = NO; [channelListDialog clear]; } break; } case RPL_LIST: { NSAssertReturn([m paramsCount] > 2); NSString *channel = [m paramAt:1]; NSString *userCount = [m paramAt:2]; NSString *topic = [m sequence:3]; if ([channel isEqualToString:@"*"]) { break; } TDCServerChannelListDialog *channelListDialog = [self channelListDialog]; if (channelListDialog) { [channelListDialog addChannel:channel count:userCount.integerValue topic:topic]; } break; } case RPL_LISTEND: { TDCServerChannelListDialog *channelListDialog = [self channelListDialog]; if (channelListDialog) { channelListDialog.contentAlreadyReceived = YES; } break; } case RPL_BANLIST: case RPL_INVITELIST: case RPL_EXCEPTLIST: case RPL_QUIETLIST: { NSAssertReturn([m paramsCount] > 2); NSUInteger paramsOffset = 0; /* Quiet list has an extra argument which is the string "q" */ if (numeric == RPL_QUIETLIST && m.paramsCount == 6) { paramsOffset = 1; } NSString *channelName = [m paramAt:1]; NSString *entryMask = [m paramAt:(2 + paramsOffset)]; NSString *entryAuthor = nil; NSDate *entryCreationDate = nil; BOOL extendedLine = (m.paramsCount > (4 + paramsOffset)); if (extendedLine) { entryAuthor = [m paramAt:(3 + paramsOffset)].nicknameFromHostmask; entryCreationDate = [NSDate dateWithTimeIntervalSince1970:[m paramAt:(4 + paramsOffset)].doubleValue]; } TDCChannelBanListSheet *listSheet = [windowController() windowFromWindowList:@"TDCChannelBanListSheet"]; if (listSheet) { if (listSheet.contentAlreadyReceived) { listSheet.contentAlreadyReceived = NO; [listSheet clear]; } [listSheet addEntry:entryMask setBy:entryAuthor creationDate:entryCreationDate]; return; } if (printMessage == NO) { return; } NSString *localization = nil; if (numeric == RPL_BANLIST) { localization = @"c04-d0"; } else if (numeric == RPL_INVITELIST) { localization = @"py2-qh"; } else if (numeric == RPL_EXCEPTLIST) { localization = @"ov2-ci"; } else if (numeric == RPL_QUIETLIST) { localization = @"u5z-az"; } if (extendedLine) { localization = [NSString stringWithFormat:@"IRC[%@-1]", localization]; } else { localization = [NSString stringWithFormat:@"IRC[%@-2]", localization]; } NSString *message = nil; if (extendedLine) { message = TXTLS(localization, channelName, entryMask, entryAuthor, entryCreationDate); } else { message = TXTLS(localization, channelName, entryMask); } [self print:message by:nil inChannel:nil asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; break; } case RPL_ENDOFBANLIST: case RPL_ENDOFINVITELIST: case RPL_ENDOFEXCEPTLIST: case RPL_ENDOFQUIETLIST: { TDCChannelBanListSheet *listSheet = [windowController() windowFromWindowList:@"TDCChannelBanListSheet"]; if (listSheet) { listSheet.contentAlreadyReceived = YES; break; } if (printMessage) { [self printReply:m]; } break; } case RPL_YOUREOPER: { if (self.userIsIRCop == NO) { self.userIsIRCop = YES; } else { break; } if (printMessage) { [self print:TXTLS(@"IRC[6bh-br]", m.senderNickname) by:nil inChannel:nil asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; } break; } case RPL_CHANNEL_URL: { NSAssertReturn([m paramsCount] == 3); NSAssertReturn(printMessage); NSString *channelName = [m paramAt:1]; NSString *website = [m paramAt:2]; IRCChannel *channel = [self findChannel:channelName]; if (channel == nil) { return; } [self print:TXTLS(@"IRC[8tq-g6]", website) by:nil inChannel:channel asType:TVCLogLineTypeWebsite command:m.command receivedAt:m.receivedAt]; break; } case RPL_WATCHSTAT: case RPL_WATCHLIST: { if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } break; } case RPL_REAWAY: case RPL_GONEAWAY: case RPL_NOTAWAY: { NSAssertReturn([m paramsCount] > 4); /* Present reply to the user if we have destination */ if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } /* Process reply */ NSString *nickname = [m paramAt:1]; IRCAddressBookEntry *addressBookEntry = [self findUserTrackingAddressBookEntryForNickname:nickname]; if (addressBookEntry == nil) { break; } switch (numeric) { case RPL_REAWAY: case RPL_GONEAWAY: // is away { [self modifyUserWithNickname:nickname asAway:YES]; break; } case RPL_NOTAWAY: // is no longer away { [self modifyUserWithNickname:nickname asAway:NO]; break; } default: { break; } } // switch() break; } case RPL_LOGON: case RPL_LOGOFF: { NSAssertReturn([m paramsCount] > 4); /* Present reply to the user if we have destination */ if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } /* Process reply */ NSString *nickname = [m paramAt:1]; IRCAddressBookEntry *addressBookEntry = [self findUserTrackingAddressBookEntryForNickname:nickname]; if (addressBookEntry == nil) { break; } switch (numeric) { case RPL_LOGON: // logged online { [self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusSignedOn notify:YES]; break; } case RPL_LOGOFF: // logged offline { [self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusSignedOff notify:YES]; break; } default: { break; } } // switch() break; } case RPL_NOWON: case RPL_NOWOFF: { NSAssertReturn([m paramsCount] > 4); /* Present reply to the user if we have destination */ if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } /* Process reply */ NSString *nickname = [m paramAt:1]; IRCAddressBookEntry *addressBookEntry = [self findUserTrackingAddressBookEntryForNickname:nickname]; if (addressBookEntry == nil) { break; } switch (numeric) { case RPL_NOWON: // is online { [self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusAvailable notify:NO]; break; } case RPL_NOWOFF: // is offline { [self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusNotAvailable notify:NO]; break; } default: { break; } } // switch() break; } case RPL_WATCHOFF: { if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } break; } case ERR_TOOMANYWATCH: { /* This message is always printed because Textual does not make an effort to check the maximum allowance for this command. We therefore want a user to know why tracking breaks in Textual instead of blaming it on a bug. */ if (printMessage) { [self printErrorReply:m]; } break; } // case RPL_CLEARWATCH: /* Not implemented by any IRCd */ case RPL_ENDOFWATCHLIST: { if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } break; } case RPL_MONLIST: { if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } break; } case RPL_MONONLINE: case RPL_MONOFFLINE: { NSAssertReturn([m paramsCount] == 2); /* Present reply to the user if we have destination */ if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } /* Process reply */ NSString *changedUsersString = [m paramAt:1]; NSArray *changedUsers = [changedUsersString componentsSeparatedByString:@","]; for (NSString *changedUser in changedUsers) { NSString *nickname = nil; if ([changedUser hostmaskComponents:&nickname username:NULL address:NULL onClient:self] == NO) { nickname = changedUser; } IRCAddressBookEntry *addressBookEntry = [self findUserTrackingAddressBookEntryForNickname:nickname]; if (addressBookEntry == nil) { continue; } if (numeric == RPL_MONONLINE) { // logged online [self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusSignedOn notify:YES]; } else { [self statusOfTrackedNickname:nickname changedTo:IRCAddressBookUserTrackingStatusSignedOff notify:YES]; } } break; } case ERR_MONLISTFULL: { /* See ERR_TOOMANYWATCH for reason we always print this. */ if (printMessage) { [self printErrorReply:m]; } break; } case RPL_ENDOFMONLIST: { if (printMessage) { [self printReplyToHiddenCommandResponsesQuery:m]; } break; } case RPL_TARGUMODEG: { // Ignore. 717 will take care of notification. break; } case RPL_TARGNOTIFY: { NSAssertReturn([m paramsCount] == 3); NSAssertReturn(printMessage); NSString *nickname = [m paramAt:1]; [self printDebugInformation:TXTLS(@"IRC[11i-ev]", nickname)]; break; } case RPL_UMODEGMSG: { NSAssertReturn([m paramsCount] == 4); NSAssertReturn(printMessage); NSString *nickname = [m paramAt:1]; NSString *hostmask = [m paramAt:2]; NSString *message = TXTLS(@"IRC[3yj-in]", nickname, hostmask); IRCChannel *channel = nil; if ([TPCPreferences locationToSendNotices] == TXNoticeSendLocationSelectedChannel) { channel = [mainWindow() selectedChannelOn:self]; } if (channel) { [self printDebugInformation:message inChannel:channel]; } else { [self printDebugInformationToConsole:message]; } break; } case RPL_LOGGEDIN: { NSAssertReturn([m paramsCount] == 4); [self enableCapability:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL]; if (printMessage) { [self print:[m sequence:3] by:nil inChannel:nil asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; } break; } case RPL_LOGGEDOUT: { NSAssertReturn([m paramsCount] == 3); [self resetSASLNegotiation]; if (printMessage) { [self print:[m sequence:2] by:nil inChannel:nil asType:TVCLogLineTypeDebug command:m.command receivedAt:m.receivedAt]; } break; } case RPL_SASLSUCCESS: case ERR_NICKLOCKED: case ERR_SASLFAIL: case ERR_SASLTOOLONG: case ERR_SASLABORTED: case ERR_SASLALREADY: case RPL_SASLMECHS: /* Treated as error */ { if (printMessage) { if (numeric == RPL_SASLSUCCESS) { // success [self printReply:m]; } else { [self printErrorReply:m]; } } if ([self isPendingCapabilityEnabled:ClientIRCv3SupportedCapabilityIsInSASLNegotiation]) { [self disablePendingCapability:ClientIRCv3SupportedCapabilityIsInSASLNegotiation]; [self resumeCapabilityNegotiation]; } break; } default: { /* We will handle custom WHOIS responses here because there are so many that it is impossible to cover them all above. */ /* For those that we don't handle, give a plugin a chance first. */ NSString *numericString = [NSString stringWithUnsignedInteger:numeric]; if ([sharedPluginManager().supportedServerInputCommands containsObject:numericString]) { break; } if (printMessage) { /* Output custom WHOIS response to proper target */ if (self.inWhoisResponse && m.paramsCount > 2) { [self printUnknownReply:m inChannel:[mainWindow() selectedChannelOn:self]]; break; } /* Output unknown result */ [self printUnknownReply:m]; } break; } } // switch() } - (void)receiveErrorNumericReply:(IRCMessage *)m { NSParameterAssert(m != nil); NSUInteger numeric = m.commandNumeric; BOOL printMessage = [self postReceivedMessage:m]; switch (numeric) { case ERR_NOSUCHNICK: { NSAssertReturn(printMessage); NSString *channelName = [m paramAt:1]; IRCChannel *channel = [self findChannel:channelName]; if (channel) { [self printErrorReply:m inChannel:channel withSequence:2]; } else { [self printErrorReply:m]; } break; } case ERR_NOSUCHSERVER: case ERR_NOSUCHCHANNEL: { if (printMessage) { [self printErrorReply:m]; } break; } case ERR_NICKNAMEINUSE: case ERR_ERRONEUSNICKNAME: { if (self.isLoggedIn) { if (printMessage) { [self printErrorReply:m]; } break; } [self receiveNicknameCollisionError:m]; break; } case ERR_UNAVAILRESOURCE: { NSString *target = [m paramAt:1]; if (self.isLoggedIn || [self stringIsNickname:target] == NO) { if (printMessage) { [self printErrorReply:m]; } break; } [self receiveNicknameCollisionError:m]; break; } case ERR_CANNOTSENDTOCHAN: { NSAssertReturn(printMessage); NSString *channelName = [m paramAt:1]; IRCChannel *channel = [self findChannel:channelName]; if (channel) { [self printErrorReply:m inChannel:channel withSequence:2]; } else { [self printErrorReply:m]; } break; } case ERR_ADMONLY: case ERR_BADCHANMASK: case ERR_BADCHANNAME: case ERR_BADCHANNEL: case ERR_BADCHANNELKEY: case ERR_BANNEDFROMCHAN: case ERR_CHANNELISFULL: case ERR_DELAYREJOIN: case ERR_FORBIDDENCHANNEL: case ERR_INVITEONLYCHAN: case ERR_LINKCHANNEL: case ERR_NEEDREGGEDNICK: case ERR_NOHIDING: case ERR_OPERONLY: case ERR_OPERSPVERIFY: case ERR_SECUREONLYCHAN: case ERR_THROTTLE: case ERR_TOOMANYCHANNELS: case ERR_TOOMANYJOINS: { NSString *channelName = [m paramAt:1]; IRCChannel *channel = [self findChannel:channelName]; if (channel) { channel.errorOnLastJoinAttempt = YES; /* In addition to the console, print join errors in the channel itself because user might check there. */ if (printMessage) { [self printErrorReply:m inChannel:channel withSequence:2]; } } /* Print to console */ if (printMessage) { [self printErrorReply:m]; } break; } case ERR_WHOSYNTAX: case ERR_WHOLIMEXCEED: { [self.requestedCommands recordWhoRequestClosed]; if (printMessage) { [self printErrorReply:m]; } break; } case ERR_DISABLED: case ERR_UNKNOWNCOMMAND: case ERR_NEEDMOREPARAMS: { NSString *command = [m paramAt:1]; if ([command isEqualToString:@"ISON"]) { [self.requestedCommands recordIsonRequestClosed]; } else if ([command isEqualToString:@"WHO"]) { [self.requestedCommands recordWhoRequestClosed]; } if (printMessage) { [self printErrorReply:m]; } break; } default: { if (printMessage) { [self printErrorReply:m]; } break; } } // switch() } - (void)receiveNicknameCollisionError:(IRCMessage *)m { NSParameterAssert(m != nil); if (self.isConnected == NO || self.isLoggedIn) { return; } NSArray *alternateNicknames = self.config.alternateNicknames; NSString *tryingNickname = self.tryingNicknameSentNickname; [self printDebugInformationToConsole:TXTLS(@"IRC[js3-9v]", tryingNickname)]; NSUInteger tryingNicknameNumber = self.tryingNicknameNumber; if (alternateNicknames.count > tryingNicknameNumber) { NSString *nickname = alternateNicknames[tryingNicknameNumber]; [self changeNickname:nickname]; } else { [self tryAnotherNickname]; } self.tryingNicknameNumber += 1; } - (void)tryAnotherNickname { if (self.isConnected == NO || self.isLoggedIn) { return; } /* IRCISupportInfo would not be populated by now which means we cannot use a server-specific maximum nickname length value at this point. */ const NSUInteger maximumLength = IRCProtocolDefaultNicknameMaximumLength; NSString *tryingNickname = [self.tryingNicknameSentNickname padNicknameWithCharacter:'_' maximumLength:maximumLength]; if (tryingNickname) { self.tryingNicknameSentNickname = tryingNickname; } else { self.tryingNicknameSentNickname = @"0"; } [self changeNickname:self.tryingNicknameSentNickname]; } #pragma mark - #pragma mark Autojoin - (void)startAutojoinTimer { if (self.autojoinTimer.timerIsActive) { return; } NSTimeInterval interval = [TPCPreferences autojoinDelayAfterIdentification]; if (CGFloatAreEqual(interval, 0.0)) { [self onAutojoinTimer]; return; } [self.autojoinTimer start:interval onRepeat:NO]; } - (void)stopAutojoinTimer { if (self.autojoinTimer.timerIsActive == NO) { return; } [self.autojoinTimer stop]; } - (void)onAutojoinTimer { [self startAutojoinNextJoinTimer]; } - (void)startAutojoinDelayedWarningTimer { if (self.autojoinDelayedWarningTimer.timerIsActive) { return; } [self.autojoinDelayedWarningTimer start:_autojoinDelayedWarningInterval onRepeat:YES]; } - (void)stopAutojoinDelayedWarningTimer { if (self.autojoinDelayedWarningTimer.timerIsActive == NO) { return; } [self.autojoinDelayedWarningTimer stop]; } - (void)onAutojoinDelayedWarningTimer { if (self.isLoggedIn == NO || self.config.hideAutojoinDelayedWarnings || self.autojoinDelayedWarningCount >= _autojoinDelayedWarningMaxCount) { [self stopAutojoinDelayedWarningTimer]; return; } self.autojoinDelayedWarningCount += 1; /* This message is posted to the server console and the front most channel if it is on this server. */ NSString *text = TXTLS(@"IRC[r5h-fj]"); [self printDebugInformationToConsole:text]; IRCChannel *c = [mainWindow() selectedChannelOn:self]; if (c != nil) { [self printDebugInformation:text inChannel:c]; } } - (void)startAutojoinNextJoinTimer { if (self.autojoinNextJoinTimer.timerIsActive) { return; } NSTimeInterval interval = [TPCPreferences autojoinDelayBetweenChannelJoins]; [self.autojoinNextJoinTimer start:interval onRepeat:YES]; /* Fake first event */ [self onAutojoinNextJoinTimer]; } - (void)stopAutojoinNextJoinTimer { if (self.autojoinNextJoinTimer.timerIsActive == NO) { return; } [self.autojoinNextJoinTimer stop]; self.channelsToAutojoin = nil; } - (void)onAutojoinNextJoinTimer { [self autojoinNextChannel]; } - (void)autojoinNextChannel { if (self.isAutojoining == NO) { return; } @synchronized (self.channelsToAutojoin) { NSUInteger numberOfChannelsRemaining = self.channelsToAutojoin.count; NSUInteger maximumNumberOfJoins = [TPCPreferences autojoinMaximumChannelJoins]; NSRange arrayRange; BOOL endOfArray = (numberOfChannelsRemaining <= maximumNumberOfJoins); if (endOfArray == NO) { arrayRange = NSMakeRange(0, maximumNumberOfJoins); } else { arrayRange = NSMakeRange(0, numberOfChannelsRemaining); } NSArray *channelsToJoin = [self.channelsToAutojoin subarrayWithRange:arrayRange]; [self autojoinChannels:channelsToJoin]; if (endOfArray == NO) { [self.channelsToAutojoin removeObjectsInRange:arrayRange]; } else { self.isAutojoining = NO; self.isAutojoined = YES; [self stopAutojoinNextJoinTimer]; } } } - (void)autojoinChannels:(NSArray *)channels { NSParameterAssert(channels != nil); [self joinChannels:channels]; } - (void)performAutoJoin { [self performAutoJoinInitiatedByUser:NO]; } - (void)performAutoJoinInitiatedByUser:(BOOL)initiatedByUser { if (self.isAutojoining) { return; } [self stopAutojoinDelayedWarningTimer]; if (initiatedByUser == NO) { /* Ignore previous invocations of this method */ if (self.isAutojoined) { return; } /* Ignore autojoin based on ZNC preferences */ if (self.isConnectedToZNC && self.config.zncIgnoreConfiguredAutojoin) { self.isAutojoined = YES; return; } /* Do nothing unless certain conditions are met */ if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityIsIdentifiedWithSASL] == NO) { if (self.config.autojoinWaitsForNickServ) { if (self.serverHasNickServ && self.userIsIdentifiedWithNickServ == NO) { return; } } } } NSMutableArray *channelsToAutojoin = [NSMutableArray array]; for (IRCChannel *c in self.channelList) { if (c.isChannel && c.isActive == NO) { if (c.config.autoJoin) { [channelsToAutojoin addObject:c]; } } } if (channelsToAutojoin.count == 0) { self.isAutojoining = YES; return; } self.isAutojoining = YES; @synchronized (self.channelsToAutojoin) { [channelsToAutojoin shuffle]; self.channelsToAutojoin = channelsToAutojoin; } [self startAutojoinTimer]; } #pragma mark - #pragma mark Post Events - (void)postEventToViewController:(NSString *)eventToken { if (themeSettings().js_postHandleEventNotifications == NO) { return; // Cancel operation... } [self postEventToViewController:eventToken forItem:self]; for (IRCChannel *channel in self.channelList) { [self postEventToViewController:eventToken forItem:channel]; } } - (void)postEventToViewController:(NSString *)eventToken forChannel:(IRCChannel *)channel { if (themeSettings().js_postHandleEventNotifications == NO) { return; // Cancel operation... } [self postEventToViewController:eventToken forItem:channel]; } - (void)postEventToViewController:(NSString *)eventToken forItem:(IRCTreeItem *)item { NSParameterAssert(eventToken != nil); NSParameterAssert(item != nil); if (self.isTerminating) { return; } [item.viewController evaluateFunction:@"Textual.handleEvent" withArguments:@[eventToken] onQueue:NO]; } #pragma mark - #pragma mark Timers - (void)startPongTimer { if (self.pongTimer.timerIsActive) { return; } [self.pongTimer start:_pongCheckInterval onRepeat:YES]; } - (void)stopPongTimer { if (self.pongTimer.timerIsActive == NO) { return; } [self.pongTimer stop]; } - (void)onPongTimer { if (self.isLoggedIn == NO) { [self stopPongTimer]; return; } /* Instead of stopping and starting the timer every time this changes, it it is easier to check if we should do it every timer iteration. The ability to disable this is important on PSYBNC connection because PSYBNC doesn't respond to PING commands. There are other irc daemons that don't reply to PING either and they should all be shot. */ NSTimeInterval timeSpent = [NSDate timeIntervalSinceNow:self.lastMessageReceived]; if (timeSpent >= _timeoutInterval) { /* If EOF Received when we were not expecting it, then timeout regardless of user preference once our timeout interval is reached. */ if (self.socket.EOFReceived || self.config.performDisconnectOnPongTimer) { [self printDebugInformation:TXTLS(@"IRC[bps-la]", (timeSpent / 60.0)) inChannel:nil]; XRPerformBlockSynchronouslyOnMainQueue(^{ [self disconnect]; }); return; } if (self.timeoutWarningShownToUser == NO) { self.timeoutWarningShownToUser = YES; [self printDebugInformation:TXTLS(@"IRC[gzo-54]", (timeSpent / 60.0)) inChannel:nil]; } } else if (timeSpent >= _pingInterval) { if (self.config.performPongTimer == NO) { return; } [self sendPing:self.serverAddress]; } } - (void)startReconnectTimer { if ((self.reconnectEnabledBecauseOfSleepMode && self.config.autoSleepModeDisconnect == NO) || (self.reconnectEnabledBecauseOfSleepMode == NO && self.config.autoReconnect == NO)) { return; } if (self.reconnectTimer.timerIsActive) { return; } [self.reconnectTimer start:_reconnectInterval onRepeat:YES]; } - (void)stopReconnectTimer { if (self.reconnectTimer.timerIsActive == NO) { return; } [self.reconnectTimer stop]; } - (void)onReconnectTimer { if (self.isConnecting || self.isConnected) { return; } [self connect:IRCClientConnectModeReconnect]; } - (void)startRetryTimer { if (self.retryTimer.timerIsActive) { return; } [self.retryTimer start:_retryInterval]; } - (void)stopRetryTimer { if (self.retryTimer.timerIsActive == NO) { return; } [self.retryTimer stop]; } - (void)onRetryTimer { if (self.isConnected == NO) { return; } XRPerformBlockSynchronouslyOnMainQueue(^{ __weak IRCClient *weakSelf = self; self.disconnectCallback = ^{ [weakSelf connect:IRCClientConnectModeRetry]; }; [self disconnect]; }); } #pragma mark - #pragma mark Requested Commands - (void)removeRequestedCommands { [self.requestedCommands removeCommands]; } - (void)createHiddenCommandResponses { if (self.isTerminating) { return; } if (self.rawDataLogQuery != nil) { return; } IRCChannel *query = [self findChannelOrCreate:@"Hidden Responses" isUtility:YES]; self.hiddenCommandResponsesQuery = query; [mainWindow() select:query]; [self printDebugInformation:TXTLS(@"IRC[yem-td]") inChannel:query]; } - (void)printReplyToHiddenCommandResponsesQuery:(IRCMessage *)message { IRCChannel *query = self.hiddenCommandResponsesQuery; if (query == nil) { return; } [self printReply:message inChannel:query]; } #pragma mark - #pragma mark Plugins and Scripts - (void)outputDescriptionForError:(NSError *)error forTextualCmdScriptAtPath:(NSString *)path inputString:(NSString *)inputString { NSString *filename = path.lastPathComponent; NSString *errorDescription = error.userInfo[NSAppleScriptErrorMessage]; if (errorDescription == nil) { errorDescription = error.userInfo[NSAppleScriptErrorBriefMessage]; } if (errorDescription == nil) { errorDescription = error.localizedFailureReason; } if (errorDescription == nil) { errorDescription = error.localizedDescription; } if (inputString.length == 0) { inputString = @"(no input)"; } [self printDebugInformation:TXTLS(@"IRC[2mc-h0]", filename, inputString, errorDescription)]; LogToConsoleError("%{public}@", TXTLS(@"IRC[ax0-mt]", errorDescription)); } - (void)sendTextualCmdScriptResult:(NSString *)resultString toChannel:(nullable NSString *)channel { NSParameterAssert(resultString != nil); IRCTreeItem *destination = nil; if (channel == nil) { destination = self; } else { destination = [self findChannel:channel]; } if (destination == nil) { LogToConsoleFault("A script returned a result but its destination no longer exists"); return; } resultString = resultString.trim; XRPerformBlockAsynchronouslyOnMainQueue(^{ [self inputText:resultString destination:destination]; }); } - (void)executeTextualCmdScriptInContext:(NSDictionary *)context { XRPerformBlockAsynchronouslyOnQueue([THOPluginDispatcher dispatchQueue], ^{ [self _executeTextualCmdScriptInContext:context]; }); } - (void)_executeTextualCmdScriptInContext:(NSDictionary *)context { NSParameterAssert(context != nil); NSString *inputString = context[@"inputString"]; NSString *path = context[@"path"]; NSString *targetChannel = context[@"targetChannel"]; NSParameterAssert(path != nil); NSURL *pathURL = [NSURL fileURLWithPath:path]; /* Is it AppleScript? */ if ([path hasSuffix:TPCResourceManagerScriptDocumentTypeExtension]) { BOOL isBuiltinScript = [path hasPrefix:[TPCPathInfo applicationResources]]; /* /////////////////////////////////////////////////////// */ /* Event Descriptor */ /* /////////////////////////////////////////////////////// */ NSAppleEventDescriptor *firstParameter = [NSAppleEventDescriptor descriptorWithString:inputString]; NSAppleEventDescriptor *secondParameter = [NSAppleEventDescriptor descriptorWithString:targetChannel]; NSAppleEventDescriptor *parameters = [NSAppleEventDescriptor listDescriptor]; [parameters insertDescriptor:firstParameter atIndex:1]; [parameters insertDescriptor:secondParameter atIndex:2]; ProcessSerialNumber process = { 0, kCurrentProcess }; NSAppleEventDescriptor *target = [NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber bytes:&process length:sizeof(ProcessSerialNumber)]; NSAppleEventDescriptor *handler = [NSAppleEventDescriptor descriptorWithString:@"textualcmd"]; NSAppleEventDescriptor *event = [NSAppleEventDescriptor appleEventWithEventClass:kASAppleScriptSuite eventID:kASSubroutineEvent targetDescriptor:target returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID]; [event setParamDescriptor:handler forKeyword:keyASSubroutineName]; [event setParamDescriptor:parameters forKeyword:keyDirectObject]; /* /////////////////////////////////////////////////////// */ /* Execute Event */ /* /////////////////////////////////////////////////////// */ /* NSUserAppleScriptTask expects the script to be in the Application Scripts folder which means if we want to execute scripts in the app's Resources folder, we use a regular call to NSAppleScript. It's pretty safe to say that scripts we make ourselves wont produce errors which means the logic for handling errors is ignored for scripts that are performed in the Resources folder. */ if (isBuiltinScript) { NSAppleScript *appleScript = [[NSAppleScript alloc] initWithContentsOfURL:pathURL error:NULL]; if (appleScript == nil) { return; } NSAppleEventDescriptor *result = [appleScript executeAppleEvent:event error:NULL]; if (result == nil) { return; } [self sendTextualCmdScriptResult:result.stringValue toChannel:targetChannel]; } else // isBuiltinScript { NSError *appleScriptError = nil; NSUserAppleScriptTask *appleScript = [[NSUserAppleScriptTask alloc] initWithURL:pathURL error:&appleScriptError]; if (appleScript == nil) { [self outputDescriptionForError:appleScriptError forTextualCmdScriptAtPath:path inputString:inputString]; return; } [appleScript executeWithAppleEvent:event completionHandler:^(NSAppleEventDescriptor *result, NSError *error) { if (result == nil) { [self outputDescriptionForError:error forTextualCmdScriptAtPath:path inputString:inputString]; } else { [self sendTextualCmdScriptResult:result.stringValue toChannel:targetChannel]; } }]; } return; } /* /////////////////////////////////////////////////////// */ /* Execute Shell Script */ /* /////////////////////////////////////////////////////// */ /* Build list of arguments. */ NSMutableArray *taskArguments = [NSMutableArray array]; if (targetChannel) { [taskArguments addObject:targetChannel]; } else { [taskArguments addObject:@""]; } NSArray *inputStringComponents = [inputString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; [taskArguments addObjectsFromArray:inputStringComponents]; /* Create task object */ NSError *taskError = nil; NSUserUnixTask *task = [[NSUserUnixTask alloc] initWithURL:pathURL error:&taskError]; if (task == nil) { [self outputDescriptionForError:taskError forTextualCmdScriptAtPath:path inputString:inputString]; return; } /* Prepare pipe */ NSPipe *standardOutputPipe = [NSPipe pipe]; NSFileHandle *readingPipe = standardOutputPipe.fileHandleForReading; NSFileHandle *writingPipe = standardOutputPipe.fileHandleForWriting; task.standardOutput = writingPipe; /* Try performing task */ [task executeWithArguments:taskArguments completionHandler:^(NSError *error) { if (error) { [self outputDescriptionForError:error forTextualCmdScriptAtPath:path inputString:inputString]; return; } NSData *result = [readingPipe readDataToEndOfFile]; NSString *resultString = [NSString stringWithData:result encoding:NSUTF8StringEncoding]; [self sendTextualCmdScriptResult:resultString toChannel:targetChannel]; }]; } - (void)processBundlesUserMessage:(NSString *)message command:(NSString *)command { NSParameterAssert(message != nil); [THOPluginDispatcher userInputCommandInvokedOnClient:self commandString:command messageString:message]; } - (void)processBundlesServerMessage:(IRCMessage *)message { NSParameterAssert(message != nil); [THOPluginDispatcher didReceiveServerInput:message onClient:self]; } - (BOOL)postReceivedMessage:(IRCMessage *)referenceMessage { NSParameterAssert(referenceMessage != nil); return [self postReceivedMessage:referenceMessage withText:referenceMessage.sequence destinedFor:nil]; } - (BOOL)postReceivedMessage:(IRCMessage *)referenceMessage withText:(nullable NSString *)text destinedFor:(nullable IRCChannel *)textDestination { NSParameterAssert(referenceMessage != nil); return [self postReceivedCommand:referenceMessage.command withText:text destinedFor:textDestination referenceMessage:referenceMessage]; } - (BOOL)postReceivedCommand:(NSString *)command withText:(nullable NSString *)text destinedFor:(nullable IRCChannel *)textDestination referenceMessage:(IRCMessage *)referenceMessage { NSParameterAssert(command != nil); NSParameterAssert(referenceMessage != nil); return [THOPluginDispatcher receivedCommand:command withText:text authoredBy:referenceMessage.sender destinedFor:textDestination onClient:self receivedAt:referenceMessage.receivedAt referenceMessage:referenceMessage]; } #pragma mark - #pragma mark Commands - (void)connect { [self connect:IRCClientConnectModeNormal]; } - (void)connect:(IRCClientConnectMode)connectMode { [self connect:connectMode bypassProxy:NO]; } - (void)connect:(IRCClientConnectMode)connectMode bypassProxy:(BOOL)bypassProxy { /* Do not allow a connect to occur until the current socket has completed disconnecting */ if (self.isConnecting || self.isConnected || self.isQuitting || self.isDisconnecting) { return; } /* Check if system is sleeping. */ if ([XRSystemInformation systemIsSleeping]) { LogToConsoleInfo("Refusing to connect because system is sleeping"); return; } /* Do we have somewhere to connect to? */ NSArray *servers = self.config.serverList; if (servers.count == 0) { [self printDebugInformationToConsole:TXTLS(@"IRC[iaa-0u]")]; return; } /* Begin populating configuration */ /* Temporary values take priority. When a temporary server address is specified, then the temporary port is used too, or 6667 without SSL is used. Nothing from the current server configuration is read if there is temporary server. */ NSString *serverAddress = nil; uint16_t serverPort = IRCConnectionDefaultServerPort; BOOL connectionPrefersSecuredConnection = NO; if (self.temporaryServerAddressOverride) { serverAddress = self.temporaryServerAddressOverride; if (self.temporaryServerPortOverride > 0 && self.temporaryServerPortOverride <= TXMaximumTCPPort) { serverPort = self.temporaryServerPortOverride; } } if (serverAddress.isValidInternetAddress == NO) { NSUInteger serverIndex = self.lastServerSelected; if (serverIndex == NSNotFound) { serverIndex = 0; } else { serverIndex += 1; if (serverIndex >= servers.count) { serverIndex = 0; } } self.lastServerSelected = serverIndex; IRCServer *server = servers[serverIndex]; serverAddress = server.serverAddress; serverPort = server.serverPort; connectionPrefersSecuredConnection = server.prefersSecuredConnection; self.server = server; } /* Do not wait for an actual connect before destroying the temporary store. Once its defined, its to be nil'd out no matter what. */ self.temporaryServerAddressOverride = nil; self.temporaryServerPortOverride = 0; /* Reset status */ self.connectType = connectMode; self.disconnectType = IRCClientDisconnectModeNormal; self.isConnecting = YES; /* Disable reconnect attempt but permit more */ [self stopReconnectTimer]; self.reconnectEnabled = YES; /* Present status to user */ [mainWindow() updateTitleFor:self]; if (connectMode == IRCClientConnectModeReconnect) { [self printDebugInformationToConsole:TXTLS(@"IRC[xxb-y2]")]; } else if (connectMode == IRCClientConnectModeRetry) { [self printDebugInformationToConsole:TXTLS(@"IRC[ky3-36]")]; } /* To provide user with similar behavior, when migrating -connectionPrefersIPv4 in IRCClientConfig, we set the address type to IRCConnectionAddressTypeIPv4. We check if both values are set to offer the user a warning that the preference they had has changed in a way they may not want. When user changes the address type in Server Properties, we unset -connectionPrefersIPv4 so that the warning does not appear again, ever. */ IRCConnectionAddressType addressType = self.config.addressType; if (self.config.showConnectionPrefersIPv4Warning) { [self printDebugInformation:TXTLS(@"IRC[w05-ph]")]; } [self printDebugInformationToConsole:TXTLS(@"IRC[o77-ls]", serverAddress, serverPort)]; [RZNotificationCenter() postNotificationName:IRCClientWillConnectNotification object:self]; /* Create socket */ IRCConnectionConfigMutable *socketConfig = [IRCConnectionConfigMutable new]; socketConfig.addressType = addressType; socketConfig.serverAddress = serverAddress; socketConfig.serverPort = serverPort; IRCConnectionProxyType proxyType = self.config.proxyType; /* Network.framework cannot use a custom proxy so we can only either use none or allow it to automatically configure the connection using the system proxy. */ socketConfig.connectionPrefersModernSockets = ([TPCPreferences preferModernSockets] && (proxyType == IRCConnectionProxyTypeNone || proxyType == IRCConnectionProxyTypeAutomatic)); socketConfig.cipherSuites = self.config.cipherSuites; socketConfig.connectionPrefersSecuredConnection = connectionPrefersSecuredConnection; socketConfig.connectionShouldValidateCertificateChain = self.config.validateServerCertificateChain; socketConfig.identityClientSideCertificate = self.config.identityClientSideCertificate; if (bypassProxy == NO) { socketConfig.proxyType = proxyType; if (socketConfig.proxyType == IRCConnectionProxyTypeSocks4 || socketConfig.proxyType == IRCConnectionProxyTypeSocks5 || socketConfig.proxyType == IRCConnectionProxyTypeHTTP || socketConfig.proxyType == IRCConnectionProxyTypeHTTPS) { socketConfig.proxyPort = self.config.proxyPort; socketConfig.proxyAddress = self.config.proxyAddress; socketConfig.proxyPassword = self.config.proxyPassword; socketConfig.proxyUsername = self.config.proxyUsername; } } socketConfig.floodControlDelayInterval = self.config.floodControlDelayTimerInterval; socketConfig.floodControlMaximumMessages = self.config.floodControlMaximumMessages; socketConfig.connectionPrefersModernCiphersOnly = [TPCPreferences preferModernCiphers]; self.socket = [[IRCConnection alloc] initWithConfig:socketConfig onClient:self]; [self.socket open]; /* Pass status to view controller */ [self postEventToViewController:@"serverConnecting"]; } - (void)autoConnectWithDelay:(NSUInteger)delay afterWakeUp:(BOOL)afterWakeUp { self.connectDelay = delay; if (afterWakeUp) { [self autoConnectAfterWakeUp]; } else { [self autoConnect]; } } - (void)autoConnect { NSUInteger connectDelay = self.connectDelay; if (connectDelay == 0) { [self autoConnectPerformConnect]; return; } [self performSelectorInCommonModes:@selector(autoConnectPerformConnect) withObject:nil afterDelay:connectDelay]; } - (void)autoConnectPerformConnect { if (self.isConnecting || self.isConnected) { return; } [self connect]; } - (void)autoConnectAfterWakeUp { NSUInteger connectDelay = self.connectDelay; if (connectDelay == 0) { [self autoConnectAfterWakeUpPerformConnect]; return; } [self printDebugInformationToConsole:TXTLS(@"IRC[3s6-e6]", connectDelay)]; [self performSelectorInCommonModes:@selector(autoConnectAfterWakeUpPerformConnect) withObject:nil afterDelay:connectDelay]; } - (void)autoConnectAfterWakeUpPerformConnect { if (self.isConnecting || self.isConnected) { return; } self.reconnectEnabledBecauseOfSleepMode = YES; [self connect:IRCClientConnectModeReconnect]; } - (void)disconnect { [self cancelPerformRequestsWithSelector:@selector(disconnect) object:nil]; if (self.isConnecting == NO && self.isConnected == NO) { return; } if (self.socket == nil) { return; } self.isDisconnecting = YES; [RZNotificationCenter() postNotificationName:IRCClientWillDisconnectNotification object:self]; [self.socket close]; } - (void)quit { NSString *comment = nil; if (self.disconnectType == IRCClientDisconnectModeComputerSleep) { comment = self.config.sleepModeLeavingComment; } else { comment = self.config.normalLeavingComment; } [self quitWithComment:comment]; } - (void)quitWithComment:(NSString *)comment { NSParameterAssert(comment != nil); if ((self.isConnecting == NO && self.isConnected == NO) || self.isQuitting || self.isDisconnecting) { return; } self.isQuitting = YES; [self cancelReconnect]; if (self.isTerminating == NO) { [self postEventToViewController:@"serverDisconnecting"]; } [RZNotificationCenter() postNotificationName:IRCClientWillSendQuitNotification object:self]; [self.socket clearSendQueue]; /* If -isLoggedIn is NO, then the connection does not need to be closed gracefully because the user hasn't even joined a channel yet, so who are we doing it for? */ if (self.isLoggedIn == NO) { [self disconnect]; return; } [self send:@"QUIT", comment, nil]; /* We give it two seconds before forcefully breaking so that the graceful quit with the quit message above can be performed. */ [self performSelectorInCommonModes:@selector(disconnect) withObject:nil afterDelay:2.0]; } - (void)cancelReconnect { self.reconnectEnabled = NO; self.reconnectEnabledBecauseOfSleepMode = NO; [self stopReconnectTimer]; [mainWindow() updateTitleFor:self]; } - (void)changeNickname:(NSString *)newNickname { NSParameterAssert(newNickname != nil); if (self.isConnected == NO) { return; } if (newNickname.length == 0) { return; } [self send:@"NICK", newNickname, nil]; } - (void)joinKickedChannel:(IRCChannel *)channel { [self joinChannel:channel]; } - (void)joinChannel:(IRCChannel *)channel { [self joinChannel:channel password:nil]; } - (void)joinUnlistedChannel:(NSString *)channel { [self joinUnlistedChannel:channel password:nil]; } - (void)joinChannel:(IRCChannel *)channel password:(nullable NSString *)password { NSParameterAssert(channel != nil); if (channel.isChannel == NO || channel.isActive) { return; } channel.status = IRCChannelStatusJoining; if (password == nil) { password = channel.secretKey; } [self forceJoinChannel:channel.name password:password]; } - (void)joinUnlistedChannel:(NSString *)channel password:(nullable NSString *)password { NSParameterAssert(channel != nil); if ([self stringIsChannelName:channel] == NO) { // Many IRCd (I don't know of any that don't) use "JOIN 0" as a // secret way to have the user part all channels they are in. if ([channel isEqualToString:@"0"]) { [self forceJoinChannel:channel password:password]; } return; } IRCChannel *channelPointer = [self findChannel:channel]; if (channelPointer) { [self joinChannel:channelPointer password:password]; return; } [self forceJoinChannel:channel password:password]; } - (void)forceJoinChannel:(NSString *)channel password:(nullable NSString *)password { NSParameterAssert(channel != nil); if (self.isLoggedIn == NO) { return; } if (channel.length == 0) { return; } [self send:@"JOIN", channel, password, nil]; } - (void)joinUnlistedChannelsWithStringAndSelectBestMatch:(NSString *)channels { [self joinUnlistedChannelsWithStringAndSelectBestMatch:channels passwords:nil]; } - (void)joinUnlistedChannelsWithStringAndSelectBestMatch:(NSString *)channels passwords:(nullable NSString *)passwords { NSParameterAssert(channels != nil); if (channels.length == 0) { return; } NSArray *targets = [channels componentsSeparatedByString:@","]; [self joinUnlistedChannelsAndSelectBestMatch:targets passwords:passwords]; } - (void)joinUnlistedChannelsAndSelectBestMatch:(NSArray *)channels { [self joinUnlistedChannelsAndSelectBestMatch:channels passwords:nil]; } - (void)joinUnlistedChannelsAndSelectBestMatch:(NSArray *)channels passwords:(nullable NSString *)passwords { NSParameterAssert(channels != nil); if (self.isLoggedIn == NO) { return; } if (channels.count == 0) { return; } __block BOOL performJoin = YES; __block IRCChannel *channelToSelect = nil; for (NSString *channel in channels) { if (channelToSelect == nil && [self stringIsChannelName:channel]) { channelToSelect = [self findChannelOrCreate:channel]; performJoin = (channelToSelect.isActive == NO || channels.count > 1); break; } } if (performJoin) { [self send:@"JOIN", [channels componentsJoinedByString:@","], passwords, nil]; } if (channelToSelect) { [mainWindow() select:channelToSelect]; } } - (void)joinChannels:(NSArray *)channels { NSParameterAssert(channels != nil); if (self.isLoggedIn == NO) { return; } if (channels.count == 0) { return; } NSMutableString *joinStringWithoutKey = nil; NSMutableString *joinStringWithKey = nil; NSMutableString *keyString = nil; for (IRCChannel *channel in channels) { if (channel.isChannel == NO || channel.isActive) { continue; } channel.status = IRCChannelStatusJoining; NSString *password = nil; if (password == nil) { password = channel.secretKey; } if (password.length == 0) { if (joinStringWithoutKey == nil) { joinStringWithoutKey = [NSMutableString stringWithString:channel.name]; } else { [joinStringWithoutKey appendFormat:@",%@", channel.name]; } } else { if (joinStringWithKey == nil) { joinStringWithKey = [NSMutableString stringWithString:channel.name]; } else { [joinStringWithKey appendFormat:@",%@", channel.name]; } if (keyString == nil) { keyString = [NSMutableString stringWithString:password]; } else { [keyString appendFormat:@",%@", password]; } } } if (joinStringWithoutKey) { [self send:@"JOIN", joinStringWithoutKey, nil]; } if (joinStringWithKey && keyString) { [self send:@"JOIN", joinStringWithKey, keyString, nil]; } } - (void)partUnlistedChannel:(NSString *)channel { [self partUnlistedChannel:channel withComment:nil]; } - (void)partChannel:(IRCChannel *)channel { [self partChannel:channel withComment:nil]; } - (void)partUnlistedChannel:(NSString *)channel withComment:(nullable NSString *)comment { NSParameterAssert(channel != nil); if ([self stringIsChannelName:channel] == NO) { return; } IRCChannel *channelPointer = [self findChannel:channel]; if (channelPointer == nil) { return; } [self partChannel:channelPointer withComment:comment]; } - (void)partChannel:(IRCChannel *)channel withComment:(nullable NSString *)comment { NSParameterAssert(channel != nil); if (self.isLoggedIn == NO) { return; } if (channel.isChannel == NO || channel.isActive == NO) { return; } if (comment == nil) { comment = self.config.normalLeavingComment; } [self send:@"PART", channel.name, comment, nil]; } - (void)sendWhoToChannel:(IRCChannel *)channel { [self sendWhoToChannel:channel hideResponse:NO]; } - (void)sendWhoToChannel:(IRCChannel *)channel hideResponse:(BOOL)hideResponse { NSParameterAssert(channel != nil); if (channel.isChannel == NO) { return; } [self sendWhoToChannelNamed:channel.name hideResponse:hideResponse]; } - (void)sendWhoToChannelNamed:(NSString *)channel { [self sendWhoToChannelNamed:channel hideResponse:NO]; } - (void)sendWhoToChannelNamed:(NSString *)channel hideResponse:(BOOL)hideResponse { NSParameterAssert(channel != nil); if (self.isLoggedIn == NO) { return; } if (channel.length == 0) { return; } if (hideResponse == NO) { [self.requestedCommands recordWhoRequestOpenedAsVisible]; } else { [self.requestedCommands recordWhoRequestOpened]; } [self send:@"WHO", channel, nil]; } - (void)sendWhois:(NSString *)nickname { NSParameterAssert(nickname != nil); if (self.isLoggedIn == NO) { return; } if (nickname.length == 0) { return; } [self send:@"WHOIS", nickname, nickname, nil]; } - (void)kick:(NSString *)nickname inChannel:(IRCChannel *)channel { NSParameterAssert(nickname != nil); NSParameterAssert(channel != nil); if (self.isLoggedIn == NO) { return; } if (channel.isChannel == NO || channel.isActive == NO) { return; } if (nickname.length == 0) { return; } NSString *reason = [TPCPreferences defaultKickMessage]; [self send:@"KICK", channel.name, nickname, reason, nil]; } - (void)toggleAwayStatusWithComment:(nullable NSString *)comment { if (self.userIsAway) { [self toggleAwayStatus:NO withComment:nil]; } else { if (comment.length == 0) { comment = TXTLS(@"IRC[xog-in]"); } [self toggleAwayStatus:YES withComment:comment]; } } - (void)toggleAwayStatus:(BOOL)setAway { NSString *comment = TXTLS(@"IRC[xog-in]"); [self toggleAwayStatus:setAway withComment:comment]; } - (void)toggleAwayStatus:(BOOL)setAway withComment:(nullable NSString *)comment { NSParameterAssert(setAway == NO || comment != nil); if (self.isLoggedIn == NO) { return; } if (setAway) { [self send:@"AWAY", comment, nil]; } else { [self send:@"AWAY", nil]; } NSString *newNickname = nil; if (setAway) { newNickname = self.config.awayNickname; self.preAwayUserNickname = self.userNickname; } else { newNickname = self.preAwayUserNickname; self.preAwayUserNickname = nil; /* If we have an away nickname configured but no preAwayNickname set, then use the configured nickname instead. User probably was on bouncer and relaunched Textual, losing preAwayNickname.*/ if (newNickname == nil && self.config.awayNickname.length > 0) { newNickname = self.config.nickname; } } if (newNickname) { [self changeNickname:newNickname]; } } - (void)presentCertificateTrustInformation { if (self.isSecured == NO) { return; } [self.socket openSecuredConnectionCertificateModal]; } - (void)requestModesForChannel:(IRCChannel *)channel { [self sendModes:nil withParameters:nil inChannel:channel]; } - (void)requestModesForChannelNamed:(NSString *)channel { [self sendModes:nil withParameters:nil inChannelNamed:channel]; } - (void)sendModes:(nullable NSString *)modeSymbols withParameters:(nullable NSArray *)parameters inChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); [self sendModes:modeSymbols withParameters:parameters inChannelNamed:channel.name]; } - (void)sendModes:(nullable NSString *)modeSymbols withParametersString:(nullable NSString *)parametersString inChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); [self sendModes:modeSymbols withParametersString:parametersString inChannelNamed:channel.name]; } - (void)sendModes:(nullable NSString *)modeSymbols withParameters:(nullable NSArray *)parameters inChannelNamed:(NSString *)channel { NSString *parametersString = [parameters componentsJoinedByString:@" "]; [self sendModes:modeSymbols withParametersString:parametersString inChannelNamed:channel]; } - (void)sendModes:(nullable NSString *)modeSymbols withParametersString:(nullable NSString *)parametersString inChannelNamed:(NSString *)channel { NSParameterAssert(channel != nil); if (self.isLoggedIn == NO) { return; } if (channel.length == 0) { return; } [self send:@"MODE", channel, modeSymbols, parametersString, nil]; } - (void)sendPing:(NSString *)tokenString { NSParameterAssert(tokenString != nil); if (self.isConnected == NO) { return; } [self send:@"PING", tokenString, nil]; } - (void)sendPong:(NSString *)tokenString { NSParameterAssert(tokenString != nil); if (self.isConnected == NO) { return; } [self send:@"PONG", tokenString, nil]; } - (void)sendInviteTo:(NSString *)nickname toJoinChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if (channel.isChannel == NO) { return; } [self sendInviteTo:nickname toJoinChannelNamed:channel.name]; } - (void)sendInviteTo:(NSString *)nickname toJoinChannelNamed:(NSString *)channel { NSParameterAssert(nickname != nil); NSParameterAssert(channel != nil); if (nickname.length == 0 || channel.length == 0) { return; } [self send:@"INVITE", nickname, channel, nil]; } - (void)requestTopicForChannel:(IRCChannel *)channel { [self sendTopicTo:nil inChannel:channel]; } - (void)requestTopicForChannelNamed:(NSString *)channel { [self sendTopicTo:nil inChannelNamed:channel]; } - (void)sendTopicTo:(nullable NSString *)topic inChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if (channel.isChannel == NO || channel.isActive == NO) { return; } [self sendTopicTo:topic inChannelNamed:channel.name]; } - (void)sendTopicTo:(nullable NSString *)topic inChannelNamed:(NSString *)channel { NSParameterAssert(channel != nil); if (self.isLoggedIn == NO) { return; } if (channel.length == 0) { return; } [self send:@"TOPIC", channel, topic, nil]; } - (void)sendCapability:(NSString *)subcommand data:(nullable NSString *)data { NSParameterAssert(subcommand != nil); if (self.isConnected == NO) { return; } [self send:@"CAP", subcommand, data, nil]; } - (void)sendCapabilityAuthenticate:(NSString *)data { NSParameterAssert(data != nil); if (self.isConnected == NO) { return; } if (data.length == 0) { return; } [self send:@"AUTHENTICATE", data, nil]; } - (void)sendIsonForNicknames:(NSArray *)nicknames { [self sendIsonForNicknames:nicknames hideResponse:NO]; } - (void)sendIsonForNicknames:(NSArray *)nicknames hideResponse:(BOOL)hideResponse { NSParameterAssert(nicknames != nil); /* Split nicknames into fixed number per-command in case there are a lot or are long. */ /* June 11, 2018: Disabled this because Textual expects nicknames to appear in ISON response or they are considered offline. If we chunk out results, then user may disappear in one ISON response and then appear in another. The long term solution is to stop relying on ISON but no idea when that will come. */ // [nicknames enumerateSubarraysOfSize:8 usingBlock:^(NSArray *objects, BOOL *stop) { [self _sendIsonForNicknames:nicknames hideResponse:hideResponse]; // }]; } - (void)_sendIsonForNicknames:(NSArray *)nicknames hideResponse:(BOOL)hideResponse { NSParameterAssert(nicknames != nil); if (self.isLoggedIn == NO) { return; } if (nicknames.count == 0) { return; } if (hideResponse == NO) { [self.requestedCommands recordIsonRequestOpenedAsVisible]; } else { [self.requestedCommands recordIsonRequestOpened]; } NSString *nicknamesString = [nicknames componentsJoinedByString:@" "]; [self send:@"ISON", nicknamesString, nil]; } - (void)requestChannelList { if (self.isLoggedIn == NO) { return; } [self send:@"LIST", nil]; } - (void)sendPassword:(NSString *)password { NSParameterAssert(password != nil); if (self.isConnected == NO) { return; } if (password.length == 0) { return; } [self send:@"PASS", password, nil]; } - (void)modifyWatchListBy:(BOOL)adding nicknames:(NSArray *)nicknames { NSParameterAssert(nicknames != nil); /* Split nicknames into fixed number per-command in case there are a lot or are long. */ [nicknames enumerateSubarraysOfSize:8 usingBlock:^(NSArray *objects, BOOL *stop) { [self _modifyWatchListBy:adding nicknames:objects]; }]; } - (void)_modifyWatchListBy:(BOOL)adding nicknames:(NSArray *)nicknames { NSParameterAssert(nicknames != nil); if (self.isLoggedIn == NO) { return; } if (nicknames.count == 0) { return; } NSString *modifier = nil; if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityMonitorCommand]) { if (adding) { modifier = @"+"; } else { modifier = @"-"; } NSString *nicknamesString = [nicknames componentsJoinedByString:@","]; [self send:@"MONITOR", modifier, nicknamesString, nil]; } else if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityWatchCommand]) { if (adding) { modifier = @" +"; } else { modifier = @" -"; } NSString *nicknamesString = [nicknames componentsJoinedByString:modifier]; [self send:@"WATCH", [modifier stringByAppendingString:nicknamesString], nil]; } } #pragma mark - #pragma mark File Transfers - (void)notifyFileTransfer:(TXNotificationType)type nickname:(NSString *)nickname filename:(NSString *)filename filesize:(uint64_t)totalFilesize requestIdentifier:(NSString *)identifier { NSParameterAssert(nickname != nil); NSParameterAssert(filename != nil); NSParameterAssert(identifier != nil); NSString *description = nil; switch (type) { case TXNotificationTypeFileTransferSendSuccessful: { description = TXTLS(@"Notifications[fhn-dd]", filename, totalFilesize); break; } case TXNotificationTypeFileTransferReceiveSuccessful: { description = TXTLS(@"Notifications[oqh-pn]", filename, totalFilesize); break; } case TXNotificationTypeFileTransferSendFailed: { description = TXTLS(@"Notifications[9r4-cq]", filename); break; } case TXNotificationTypeFileTransferReceiveFailed: { description = TXTLS(@"Notifications[cqq-ci]", filename); break; } case TXNotificationTypeFileTransferReceiveRequested: { description = TXTLS(@"Notifications[wik-wq]", filename, totalFilesize); break; } default: { break; } } NSDictionary *info = @{ @"isFileTransferNotification" : @(YES), @"fileTransferUniqueIdentifier" : identifier, @"fileTransferNotificationType" : @(type) }; [self notifyEvent:type lineType:TVCLogLineTypeUndefined target:nil nickname:nickname text:description userInfo:info]; } - (void)receivedDCCQuery:(IRCMessage *)m text:(NSString *)text ignoreInfo:(nullable IRCAddressBookEntry *)ignoreInfo { NSParameterAssert(m != nil); NSParameterAssert(text != nil); if (self.isLoggedIn == NO) { return; } /* Do not continue if the user has configured an ignore for file transfers */ if (ignoreInfo.ignoreFileTransferRequests) { return; } /* Do not continue if we are not the target */ if ([self nicknameIsMyself:[m paramAt:0]] == NO) { return; } /* Record information */ NSString *sender = m.senderNickname; NSMutableString *textMutable = [text mutableCopy]; NSString *subcommand = textMutable.uppercaseGetToken; BOOL isSendRequest = ([subcommand isEqualToString:@"SEND"]); BOOL isResumeRequest = ([subcommand isEqualToString:@"RESUME"]); BOOL isAcceptRequest = ([subcommand isEqualToString:@"ACCEPT"]); if (isSendRequest == NO && isResumeRequest == NO && isAcceptRequest == NO) { return; } NSString *section1 = nil; if ([textMutable hasPrefix:@"\""]) { section1 = textMutable.tokenInsideQuotes; } else { section1 = textMutable.token; } NSString *section2 = textMutable.token; NSString *section3 = textMutable.token; NSString *section4 = textMutable.token; NSString *section5 = textMutable.token; /* Trim whitespaces in case someone tries to send blank spaces in a quoted string for filename. */ section1 = section1.trim; /* Remove T from in front of token if it is there. */ if (isSendRequest) { if ([section5 hasPrefix:@"T"]) { section5 = [section5 substringFromIndex:1]; } } else if (isAcceptRequest || isResumeRequest) { if ([section4 hasPrefix:@"T"]) { section4 = [section4 substringFromIndex:1]; } } /* Valid values? */ if ( section1.length == 0 || section2.length == 0 || (section4.length == 0 && isSendRequest)) { return; } /* Start data association. */ NSString *hostAddress = nil; NSString *hostPort = nil; NSString *filename = nil; NSString *filesize = nil; NSString *transferToken = nil; /* Match data variables. */ if (isSendRequest) { /* Get normal information */ filename = section1.safeFilename; filesize = section4; hostPort = section3; transferToken = section5; /* Translate host address */ if (section2.numericOnly) { long long a = section2.longLongValue; NSInteger w = (a & 0xff); a >>= 8; NSInteger x = (a & 0xff); a >>= 8; NSInteger y = (a & 0xff); a >>= 8; NSInteger z = (a & 0xff); hostAddress = [NSString stringWithFormat:@"%ld.%ld.%ld.%ld", (long)z, (long)y, (long)x, (long)w]; } else { hostAddress = section2; } } else if (isResumeRequest || isAcceptRequest) { filename = section1.safeFilename; filesize = section3; hostPort = section2; transferToken = section4; hostAddress = nil; } if (transferToken && transferToken.length == 0) { transferToken = nil; } /* Important checks */ if (transferToken.length > 0 && transferToken.numericOnly == NO) { LogToConsoleError("Fatal error: Received transfer token that is not a number"); goto present_error; } NSInteger hostPortInt = hostPort.integerValue; if (hostPortInt == 0 && transferToken == nil) { LogToConsoleError("Fatal error: Port cannot be zero without a transfer token"); goto present_error; } else if (hostPortInt < 0 || hostPortInt > TXMaximumTCPPort) { LogToConsoleError("Fatal error: Port cannot be less than zero or greater than 65535"); goto present_error; } long long filesizeInt = filesize.longLongValue; if (filesizeInt <= 0 || filesizeInt > powl(1000, 4)) { // 1 TB LogToConsoleError("Fatal error: Filesize is silly"); goto present_error; } /* Process individual commands */ if (isSendRequest) { /* DCC SEND [token] */ if (transferToken) { TDCFileTransferDialogTransferController *e = [[self fileTransferController] fileTransferSenderMatchingToken:transferToken]; /* 0 port indicates a new request in reverse DCC */ if (hostPortInt == 0) { if (e != nil) { LogToConsoleError("Fatal error: Received reverse DCC request with token '%{public}@' but the token already exists", transferToken); goto present_error; } [self receivedDCCSend:sender filename:filename address:hostAddress port:hostPortInt filesize:filesizeInt token:transferToken]; return; } else if (e) { if (e.transferStatus != TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept) { LogToConsoleError("Fatal error: Unexpected request to begin transfer"); goto present_error; } [e didReceiveSendRequest:hostAddress hostPort:hostPortInt]; return; } } else // transferToken { /* Treat as normal DCC request */ [self receivedDCCSend:sender filename:filename address:hostAddress port:hostPort.integerValue filesize:filesize.longLongValue token:nil]; return; } } else if (isResumeRequest || isAcceptRequest) { TDCFileTransferDialogTransferController *e = nil; if (transferToken && hostPortInt == 0) { e = [[self fileTransferController] fileTransferSenderMatchingToken:transferToken]; } else if (transferToken == nil && hostPortInt > 0) { e = [[self fileTransferController] fileTransferMatchingPort:hostPortInt]; } if (e == nil) { LogToConsoleError("Fatal error: Could not locate file transfer that matches resume request"); goto present_error; } if ((isResumeRequest && (e.transferStatus != TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept && e.transferStatus != TDCFileTransferDialogTransferStatusIsListeningAsSender)) || (isAcceptRequest && e.transferStatus != TDCFileTransferDialogTransferStatusWaitingForResumeAccept)) { LogToConsoleError("Fatal error: Bad transfer status"); goto present_error; } if (isResumeRequest) { [e didReceiveResumeRequest:filesizeInt]; } else { [e didReceiveResumeAccept:filesizeInt]; } return; } // Report an error present_error: [self print:TXTLS(@"IRC[y3w-la]", sender) by:nil inChannel:nil asType:TVCLogLineTypeDCCFileTransfer command:TVCLogLineDefaultCommandValue]; } - (void)receivedDCCSend:(NSString *)nickname filename:(NSString *)filename address:(NSString *)address port:(uint16_t)port filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken { NSParameterAssert(nickname != nil); NSParameterAssert(filename != nil); NSParameterAssert(address != nil); NSString *message = TXTLS(@"IRC[snf-45]", nickname, filename, totalFilesize); [self print:message by:nil inChannel:nil asType:TVCLogLineTypeDCCFileTransfer command:TVCLogLineDefaultCommandValue]; if ([TPCPreferences fileTransferRequestReplyAction] == TXFileTransferRequestReplyIgnore) { return; } NSString *addedRequest = [[self fileTransferController] addReceiverForClient:self nickname:nickname address:address port:port filename:filename filesize:totalFilesize token:transferToken]; if (addedRequest == nil) { return; } [self notifyFileTransfer:TXNotificationTypeFileTransferReceiveRequested nickname:nickname filename:filename filesize:totalFilesize requestIdentifier:addedRequest]; } - (void)sendFileResume:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken { NSParameterAssert(nickname != nil); NSParameterAssert(filename != nil); NSString *escapedFilename = [self DCCSendEscapeFilename:filename]; NSString *stringToSend = nil; if (transferToken) { stringToSend = [NSString stringWithFormat:@"%@ %hu %lli %@", escapedFilename, port, totalFilesize, transferToken]; } else { stringToSend = [NSString stringWithFormat:@"%@ %hu %lli", escapedFilename, port, totalFilesize]; } [self sendCTCPQuery:nickname command:@"DCC RESUME" text:stringToSend]; } - (void)sendFileResumeAccept:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken { NSParameterAssert(nickname != nil); NSParameterAssert(filename != nil); NSString *escapedFilename = [self DCCSendEscapeFilename:filename]; NSString *stringToSend = nil; if (transferToken) { stringToSend = [NSString stringWithFormat:@"%@ %hu %lli %@", escapedFilename, port, totalFilesize, transferToken]; } else { stringToSend = [NSString stringWithFormat:@"%@ %hu %lli", escapedFilename, port, totalFilesize]; } [self sendCTCPQuery:nickname command:@"DCC ACCEPT" text:stringToSend]; } - (void)sendFile:(NSString *)nickname port:(uint16_t)port filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken { NSParameterAssert(nickname != nil); NSParameterAssert(filename != nil); NSString *address = [self DCCTransferAddress]; if (address == nil) { return; } NSString *escapedFilename = [self DCCSendEscapeFilename:filename]; NSString *stringToSend = nil; if (transferToken.length > 0) { stringToSend = [NSString stringWithFormat:@"%@ %@ %hu %lli %@", escapedFilename, address, port, totalFilesize, transferToken]; } else { stringToSend = [NSString stringWithFormat:@"%@ %@ %hu %lli", escapedFilename, address, port, totalFilesize]; } [self sendCTCPQuery:nickname command:@"DCC SEND" text:stringToSend]; NSString *message = TXTLS(@"IRC[ags-s8]", nickname, filename, totalFilesize); [self print:message by:nil inChannel:nil asType:TVCLogLineTypeDCCFileTransfer command:TVCLogLineDefaultCommandValue]; } - (NSString *)DCCSendEscapeFilename:(NSString *)filename { NSParameterAssert(filename != nil); NSString *filenameEscaped = filename.safeFilename; if ([filenameEscaped contains:@" "] == NO) { return filenameEscaped; } /* Escape double quotes because the filename will be wrapped. February 20, 2017: Maybe we should replace the double quote with another character or remove completely? Untested how other clients will handle an escaped double quote. */ filenameEscaped = [filenameEscaped stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; return [NSString stringWithFormat:@"\"%@\"", filenameEscaped]; } - (nullable NSString *)DCCTransferAddress { NSString *address = [self fileTransferController].IPAddress; if (address == nil) { return nil; } if (address.IPv6Address) { return address; } NSArray *addressOctets = [address componentsSeparatedByString:@"."]; if (addressOctets.count != 4) { LogToConsoleError("User configured a silly IP address"); return nil; } NSInteger w = [addressOctets[0] integerValue]; NSInteger x = [addressOctets[1] integerValue]; NSInteger y = [addressOctets[2] integerValue]; NSInteger z = [addressOctets[3] integerValue]; unsigned long long a = 0; a |= w; a <<= 8; a |= x; a <<= 8; a |= y; a <<= 8; a |= z; return [NSString stringWithFormat:@"%llu", a]; } #pragma mark - #pragma mark Command Queue - (NSString *)descriptionForTimedCommand:(IRCTimedCommand *)timedCommand { NSParameterAssert(timedCommand != nil); NSString *timerInterval = TXHumanReadableTimeInterval(timedCommand.timerInterval, NO, 0); NSString *timeRemaining = TXHumanReadableTimeInterval(timedCommand.timeRemaining, NO, 0); NSString *timerStatus = nil; if (timedCommand.timerIsActive == NO) { timerStatus = TXTLS(@"IRC[ww4-sn]"); } else { timerStatus = TXTLS(@"IRC[bhz-9e]"); } if (timedCommand.repeatTimer == NO) { return TXTLS(@"IRC[4n6-2x]", timedCommand.identifier, timerStatus, timerInterval, timeRemaining, timedCommand.command); } else { NSUInteger repeatLimit = timedCommand.iterations; NSString *repeatLimitDescriptor = nil; if (repeatLimit == 0) { repeatLimitDescriptor = TXTLS(@"IRC[o26-ae]"); } else { repeatLimitDescriptor = [NSString stringWithUnsignedInteger:repeatLimit]; } return TXTLS(@"IRC[uw0-v2]", timedCommand.identifier, timerStatus, timerInterval, timeRemaining, repeatLimitDescriptor, timedCommand.currentIteration, timedCommand.command); } } - (nullable IRCTimedCommand *)timedCommandWithIdentifier:(NSString *)identifier { NSParameterAssert(identifier != nil); @synchronized (self.timedCommands) { return self.timedCommands[identifier]; } } - (NSArray *)listOfTimedCommands { @synchronized (self.timedCommands) { return self.timedCommands.allValues; } } - (void)addTimedCommand:(IRCTimedCommand *)timedCommand { NSParameterAssert(timedCommand != nil); @synchronized (self.timedCommands) { self.timedCommands[timedCommand.identifier] = timedCommand; } } - (void)removeTimedCommands { @synchronized (self.timedCommands) { [self.timedCommands removeAllObjects]; } } - (void)removeTimedCommand:(IRCTimedCommand *)timedCommand { NSParameterAssert(timedCommand != nil); @synchronized (self.timedCommands) { [self.timedCommands removeObjectForKey:timedCommand.identifier]; } } - (void)stopTimedCommand:(IRCTimedCommand *)timedCommand { NSParameterAssert(timedCommand != nil); [timedCommand stop]; } - (void)startTimedCommand:(IRCTimedCommand *)timedCommand interval:(NSUInteger)timerInterval { NSParameterAssert(timedCommand != nil); [self startTimedCommand:timedCommand interval:timerInterval onRepeat:NO iterations:0]; } - (void)startTimedCommand:(IRCTimedCommand *)timedCommand interval:(NSUInteger)timerInterval onRepeat:(BOOL)repeatTimer { NSParameterAssert(timedCommand != nil); [self startTimedCommand:timedCommand interval:timerInterval onRepeat:repeatTimer iterations:0]; } - (void)startTimedCommand:(IRCTimedCommand *)timedCommand interval:(NSUInteger)timerInterval onRepeat:(BOOL)repeatTimer iterations:(NSUInteger)iterations { NSParameterAssert(timedCommand != nil); [timedCommand start:timerInterval onRepeat:repeatTimer iterations:iterations]; } - (BOOL)restartTimedCommand:(IRCTimedCommand *)timedCommand { NSParameterAssert(timedCommand != nil); return [timedCommand restart]; } - (void)onTimedCommand:(IRCTimedCommand *)timedCommand { NSParameterAssert(timedCommand != nil); /* Remove timer */ if (timedCommand.timerIsActive == NO) { [self removeTimedCommand:timedCommand]; } /* The -channelId is only a suggestion. It's okay if this returns nil. The channel is what was selected at the time that the timer was created. -sendCommand:completeTarget:target: may very well ignore the channel we give it, even if it's non-nil, depending on format of command. */ IRCChannel *channel = (IRCChannel *)[worldController() findItemWithId:timedCommand.channelId]; [self sendCommand:timedCommand.command completeTarget:YES target:channel.name]; } #pragma mark - #pragma mark User Tracking - (void)clearTrackedUsers { [self.trackedUsers clearTrackedUsers]; } - (void)statusOfTrackedNickname:(NSString *)nickname changedTo:(IRCAddressBookUserTrackingStatus)newStatus { [self statusOfTrackedNickname:nickname changedTo:newStatus notify:NO]; } - (void)statusOfTrackedNickname:(NSString *)nickname changedTo:(IRCAddressBookUserTrackingStatus)newStatus notify:(BOOL)notify { NSParameterAssert(nickname != nil); [self.trackedUsers statusOfTrackedNickname:nickname changedTo:newStatus]; if (notify) { [self notifyStatusOfTrackedNickname:nickname changedTo:newStatus]; } } - (void)notifyStatusOfTrackedNickname:(NSString *)nickname changedTo:(IRCAddressBookUserTrackingStatus)newStatus { NSParameterAssert(nickname != nil); NSString *message = nil; if (newStatus == IRCAddressBookUserTrackingStatusSignedOn) { message = TXTLS(@"Notifications[xk2-1l]", nickname); } else if (newStatus == IRCAddressBookUserTrackingStatusSignedOff) { message = TXTLS(@"Notifications[rif-9r]", nickname); } else if (newStatus == IRCAddressBookUserTrackingStatusAvailable) { message = TXTLS(@"Notifications[97r-0l]", nickname); } if (message == nil) { return; } [self notifyEvent:TXNotificationTypeAddressBookMatch lineType:TVCLogLineTypeNotice target:nil nickname:nickname text:message]; } - (void)populateISONTrackedUsersList { if (self.isLoggedIn == NO) { return; } /* Additions & Removals for WATCH command. ISON does not access these. */ NSMutableArray *watchAdditions = [NSMutableArray array]; NSMutableArray *watchRemovals = [NSMutableArray array]; /* Compare configuration to the list of tracked nicknames. * Nicknames that are new are added to watchAdditions */ NSDictionary *trackedUsersOld = self.trackedUsers.trackedUsers; NSMutableArray *trackedUsersNew = [NSMutableArray array]; for (IRCAddressBookEntry *g in self.config.ignoreList) { if (g.trackUserActivity == NO) { continue; } NSString *trackingNickname = g.trackingNickname; IRCAddressBookUserTrackingStatus trackingStatus = [self.trackedUsers statusOfUser:trackingNickname]; if (trackingStatus != IRCAddressBookUserTrackingStatusUnknown) { [trackedUsersNew addObject:trackingNickname]; continue; } [watchAdditions addObject:trackingNickname]; [self.trackedUsers _addTrackedUser:trackingNickname]; } /* Compare old list of tracked nicknames to new list to find those that no longer appear. Mark those for removal. */ for (NSString *trackedUser in trackedUsersOld) { if ([trackedUsersNew containsObjectIgnoringCase:trackedUser]) { continue; } [watchRemovals addObject:trackedUser]; [self.trackedUsers _removeTrackedUser:trackedUser]; } /* Set new entries */ [self modifyWatchListBy:YES nicknames:watchAdditions]; [self modifyWatchListBy:NO nicknames:watchRemovals]; [self startISONTimer]; } - (void)startISONTimer { if (self.isonTimer.timerIsActive) { return; } [self.isonTimer start:_isonCheckInterval onRepeat:YES]; [self startWhoTimer]; } - (void)stopISONTimer { if (self.isonTimer.timerIsActive == NO) { return; } [self.isonTimer stop]; [self stopWhoTimer]; } - (void)onISONTimer { if (self.isLoggedIn == NO || self.isBrokenIRCd_aka_Twitch) { return; } NSMutableArray *nicknames = [NSMutableArray array]; // Request ISON status for tracked users if (self.supportsAdvancedTracking == NO) { for (NSString *trackedUser in self.trackedUsers.trackedUsers) { [nicknames addObject:trackedUser]; } } // Request ISON status for private messages for (IRCChannel *channel in self.channelList) { if (channel.privateMessage) { [nicknames addObject:channel.name]; } } [self sendIsonForNicknames:nicknames hideResponse:YES]; } - (void)startWhoTimer { if (self.whoTimer.timerIsActive) { return; } [self.whoTimer start:_whoCheckInterval onRepeat:YES]; } - (void)stopWhoTimer { if (self.whoTimer.timerIsActive == NO) { return; } [self.whoTimer stop]; } - (void)onWhoTimer { if (self.isLoggedIn == NO || self.isBrokenIRCd_aka_Twitch) { return; } NSArray *channelList = self.channelList; [self sendTimedWhoRequestsToChannels:channelList]; } - (void)sendTimedWhoRequestsToChannels:(NSArray *)channelList { NSParameterAssert(channelList != nil); if (self.isLoggedIn == NO || self.isBrokenIRCd_aka_Twitch) { return; } #define _maximumChannelCountPerWhoBatchRequest 4 #define _maximumSingleChannelSizePerWhoBatchRequest 5000 #define _maximumTotalChannelSizePerWhoBatchRequest 2000 NSUInteger channelCount = channelList.count; if (channelCount == 0) { return; } NSUInteger startingPosition = self.lastWhoRequestChannelListIndex; NSUInteger endingPosition = (startingPosition + _maximumChannelCountPerWhoBatchRequest); if (startingPosition >= channelCount) { startingPosition = 0; } if (endingPosition >= channelCount) { endingPosition = (channelCount - 1); } NSUInteger totalMemberCount = 0; NSMutableArray *channelsToQuery = nil; for (NSUInteger channelIndex = startingPosition; channelIndex <= endingPosition; channelIndex++) { IRCChannel *channel = channelList[channelIndex]; if (channel.isActive == NO || channel.isChannel == NO) { continue; } /* Update internal state of flag */ BOOL sentInitialWhoRequest = channel.sentInitialWhoRequest; if (sentInitialWhoRequest == NO) { channel.sentInitialWhoRequest = YES; } /* continue to next channel and do not break so that the -sentInitialWhoRequest flag of all channels can be updated. */ if (self.config.sendWhoCommandRequestsToChannels == NO) { continue; } /* Perform comparisons to know whether channel is acceptable */ NSUInteger numberOfMembers = channel.numberOfMembers; if (sentInitialWhoRequest == NO) { if (numberOfMembers > _maximumSingleChannelSizePerWhoBatchRequest) { continue; } } else { if ([self isCapabilityEnabled:ClientIRCv3SupportedCapabilityAwayNotify]) { continue; } if (numberOfMembers > [TPCPreferences trackUserAwayStatusMaximumChannelSize]) { continue; } } /* Add channel to list */ if (channelsToQuery == nil) { channelsToQuery = [NSMutableArray new]; } [channelsToQuery addObject:channel]; /* Update total number of members and maybe break loop */ totalMemberCount += numberOfMembers; if (totalMemberCount > _maximumTotalChannelSizePerWhoBatchRequest) { endingPosition = channelIndex; break; } } self.lastWhoRequestChannelListIndex = (endingPosition + 1); /* Send WHO requests */ if (channelsToQuery == nil) { return; } for (IRCChannel *channel in channelsToQuery) { [self sendWhoToChannel:channel hideResponse:YES]; } #undef _maximumChannelCountPerWhoBatchRequest #undef _maximumSingleChannelSizePerWhoBatchRequest #undef _maximumTotalChannelSizePerWhoBatchRequest } - (void)updateUserTrackingStatusForEntry:(IRCAddressBookEntry *)addressBookEntry withMessage:(IRCMessage *)message { NSParameterAssert(addressBookEntry != nil); NSParameterAssert(message != nil); if (self.supportsAdvancedTracking) { return; } IRCAddressBookUserTrackingStatus trackingStatus = [self.trackedUsers statusOfEntry:addressBookEntry]; if (trackingStatus == IRCAddressBookUserTrackingStatusUnknown) { return; } BOOL ison = (trackingStatus == IRCAddressBookUserTrackingStatusAvailable); /* Notification Type: JOIN Command */ if ([message.command isEqualToStringIgnoringCase:@"JOIN"]) { if (ison == NO) { [self statusOfTrackedNickname:message.senderNickname changedTo:IRCAddressBookUserTrackingStatusSignedOn notify:YES]; } return; } /* Notification Type: QUIT Command */ if ([message.command isEqualToStringIgnoringCase:@"QUIT"]) { if (ison) { [self statusOfTrackedNickname:message.senderNickname changedTo:IRCAddressBookUserTrackingStatusSignedOff notify:YES]; } return; } /* Notification Type: NICK Command */ if ([message.command isEqualToStringIgnoringCase:@"NICK"]) { if (ison) { [self statusOfTrackedNickname:message.senderNickname changedTo:IRCAddressBookUserTrackingStatusSignedOff notify:YES]; } else { [self statusOfTrackedNickname:message.senderNickname changedTo:IRCAddressBookUserTrackingStatusSignedOn notify:YES]; } return; } } #pragma mark - #pragma mark Channel Ban List Dialog - (void)createChannelInviteExceptionListSheet { [self createChannelBanListSheet:TDCChannelBanListSheetEntryTypeInviteException]; } - (void)createChannelBanExceptionListSheet { [self createChannelBanListSheet:TDCChannelBanListSheetEntryTypeBanException]; } - (void)createChannelBanListSheet { [self createChannelBanListSheet:TDCChannelBanListSheetEntryTypeBan]; } - (void)createChannelQuietListSheet { [self createChannelBanListSheet:TDCChannelBanListSheetEntryTypeQuiet]; } - (void)createChannelBanListSheet:(TDCChannelBanListSheetEntryType)entryType { [windowController() popMainWindowSheetIfExists]; IRCChannel *c = mainWindow().selectedChannel; if (c == nil) { return; } TDCChannelBanListSheet *listSheet = [[TDCChannelBanListSheet alloc] initWithEntryType:entryType inChannel:c]; if (listSheet == nil) { return; } listSheet.delegate = (id)self; listSheet.window = mainWindow(); [listSheet start]; [windowController() addWindowToWindowList:listSheet]; } - (void)channelBanListSheetOnUpdate:(TDCChannelBanListSheet *)sender { IRCChannel *channel = sender.channel; if (channel == nil) { return; } NSString *modeSend = [NSString stringWithFormat:@"+%@", sender.modeSymbol]; [self sendModes:modeSend withParametersString:nil inChannel:channel]; } - (void)channelBanListSheetWillClose:(TDCChannelBanListSheet *)sender { IRCChannel *channel = sender.channel; if (channel == nil) { return; } NSArray *listOfChanges = sender.listOfChanges; for (NSString *change in listOfChanges) { [self sendModes:change withParametersString:nil inChannel:channel]; } [windowController() removeWindowFromWindowList:sender]; } #pragma mark - #pragma mark Network Channel List Dialog - (NSString *)channelListDialogWindowKey { return [NSString stringWithFormat:@"TDCServerChannelListDialog -> %@", self.uniqueIdentifier]; } - (nullable TDCServerChannelListDialog *)channelListDialog { return [windowController() windowFromWindowList:[self channelListDialogWindowKey]]; } - (void)createChannelListDialog { if ([windowController() maybeBringWindowForward:[self channelListDialogWindowKey]]) { return; // The window was brought forward already. } TDCServerChannelListDialog *channelListDialog = [[TDCServerChannelListDialog alloc] initWithClient:self]; channelListDialog.delegate = (id)self; [channelListDialog show]; [windowController() addWindowToWindowList:channelListDialog withDescription:[self channelListDialogWindowKey]]; } - (void)serverChannelListDialogOnUpdate:(TDCServerChannelListDialog *)sender { [self requestChannelList]; } - (void)serverChannelListDialog:(TDCServerChannelListDialog *)sender joinChannels:(NSArray *)channels { [self joinUnlistedChannelsAndSelectBestMatch:channels]; } - (void)serverChannelDialogWillClose:(TDCServerChannelListDialog *)sender { [windowController() removeWindowFromWindowList:[self channelListDialogWindowKey]]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCClientConfig.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "IRC.h" #import "IRCAddressBook.h" #import "IRCChannelConfig.h" #import "IRCHighlightMatchCondition.h" #import "IRCNetworkList.h" #import "IRCServerPrivate.h" #import "TPCPreferencesLocal.h" #import "TLOLocalization.h" #import "IRCClientConfigInternal.h" NS_ASSUME_NONNULL_BEGIN #define IRCClientConfigDictionaryVersionLatest 710 #define IRCClientConfigFloodControlDefaultDelayIntervalLimited 2 #define IRCClientConfigFloodControlDefaultMessageCountLimited 2 // freenode gets a special case 'cause they are strict about flood control @interface IRCClientConfig () @property (readonly) BOOL prefersSecuredConnection_; @property (readonly) uint16_t serverPort_; @property (readonly, copy, nullable) NSString *serverAddress_; @property (readonly) BOOL connectionPrefersModernCiphers_; @end @implementation IRCClientConfig #pragma mark - #pragma mark Defaults - (void)populateDefaultsPreflight { if (self.initializedAsCopy) { return; } /* Even if a value is NO, include it as a default. */ /* This allows NO values to be stripped from output dictionary. */ NSMutableDictionary *defaults = [NSMutableDictionary dictionary]; defaults[@"addressType"] = @(IRCConnectionAddressTypeDefault); defaults[@"autoConnect"] = @(NO); defaults[@"autoReconnect"] = @(NO); defaults[@"autoSleepModeDisconnect"] = @(YES); defaults[@"autojoinWaitsForNickServ"] = @(NO); defaults[@"cachedLastServerTimeCapabilityReceivedAtTimestamp"] = @(0); defaults[@"cipherSuites"] = @(RCMCipherSuiteCollectionDefault); defaults[@"connectionName"] = TXTLS(@"BasicLanguage[vfu-c0]"); defaults[@"fallbackEncoding"] = @(TXDefaultFallbackStringEncoding); defaults[@"floodControlDelayTimerInterval"] = @(IRCConnectionConfigFloodControlDefaultDelayInterval); defaults[@"floodControlMaximumMessages"] = @(IRCConnectionConfigFloodControlDefaultMessageCount); defaults[@"hideAutojoinDelayedWarnings"] = @(NO); defaults[@"hideNetworkUnavailabilityNotices"] = @(NO); defaults[@"normalLeavingComment"] = TXTLS(@"BasicLanguage[1dd-0f]"); defaults[@"performDisconnectOnPongTimer"] = @(NO); defaults[@"performDisconnectOnReachabilityChange"] = @(YES); defaults[@"performPongTimer"] = @(YES); defaults[@"prefersSecuredConnection"] = @(NO); defaults[@"primaryEncoding"] = @(TXDefaultPrimaryStringEncoding); defaults[@"proxyPort"] = @(IRCConnectionDefaultProxyPort); defaults[@"proxyType"] = @(IRCConnectionProxyTypeAutomatic); defaults[@"saslAuthenticationDisableExternalMechanism"] = @(NO); defaults[@"sendAuthenticationRequestsToUserServ"] = @(NO); defaults[@"sendWhoCommandRequestsToChannels"] = @(YES); defaults[@"serverPort"] = @(IRCConnectionDefaultServerPort); defaults[@"setInvisibleModeOnConnect"] = @(NO); defaults[@"sidebarItemExpanded"] = @(YES); defaults[@"sleepModeLeavingComment"] = TXTLS(@"BasicLanguage[qi7-5y]"); defaults[@"validateServerCertificateChain"] = @(YES); defaults[@"zncIgnoreConfiguredAutojoin"] = @(NO); defaults[@"zncIgnorePlaybackNotifications"] = @(YES); defaults[@"zncIgnoreUserNotifications"] = @(NO); defaults[@"zncOnlyPlaybackLatest"] = @(YES); self->_defaults = [defaults copy]; } - (void)populateDefaultsPostflight { if (self.initializedAsCopy) { return; } SetVariableIfNil(self->_uniqueIdentifier, [NSString stringWithUUID]) SetVariableIfNil(self->_nickname, [TPCPreferences defaultNickname]) SetVariableIfNil(self->_awayNickname, [TPCPreferences defaultAwayNickname]) SetVariableIfNil(self->_username, [TPCPreferences defaultUsername]) SetVariableIfNil(self->_realName, [TPCPreferences defaultRealName]) SetVariableIfNil(self->_ignoreList, @[]) SetVariableIfNil(self->_channelList, @[]) SetVariableIfNil(self->_highlightList, @[]) SetVariableIfNil(self->_serverList, @[]) SetVariableIfNil(self->_alternateNicknames, @[]) SetVariableIfNil(self->_loginCommands, @[]) [self modifyFloodControlDefaults]; } - (void)populateDefaultsByAppendingDictionary:(NSDictionary *)defaultsToAppend { NSParameterAssert(defaultsToAppend != nil); self->_defaults = [self->_defaults dictionaryByAddingEntries:defaultsToAppend]; } - (void)modifyFloodControlDefaults { if (self.floodControlDelayTimerInterval != IRCConnectionConfigFloodControlDefaultDelayInterval || self.floodControlMaximumMessages != IRCConnectionConfigFloodControlDefaultMessageCount) { return; } BOOL haveLimitedServer = NO; for (IRCServer *server in self.serverList) { if ([server.serverAddress hasSuffix:@".freenode.net"] == NO) { continue; } haveLimitedServer = YES; break; } if (haveLimitedServer == NO) { return; } NSUInteger floodControlDelayTimerInterval = IRCClientConfigFloodControlDefaultDelayIntervalLimited; NSUInteger floodControlMaximumMessages = IRCClientConfigFloodControlDefaultMessageCountLimited; [self populateDefaultsByAppendingDictionary:@{ @"floodControlDelayTimerInterval" : @(floodControlDelayTimerInterval), @"floodControlMaximumMessages" : @(floodControlMaximumMessages) }]; self->_floodControlDelayTimerInterval = floodControlDelayTimerInterval; self->_floodControlMaximumMessages = floodControlMaximumMessages; } #pragma mark - #pragma mark Server Configuration - (instancetype)init { return [self initWithDictionary:@{}]; } - (instancetype)initWithDictionary:(NSDictionary *)dic { return [self initWithDictionary:dic ignorePrivateMessages:NO]; } - (instancetype)initWithDictionary:(NSDictionary *)dic ignorePrivateMessages:(BOOL)ignorePrivateMessages { if ((self = [super init])) { [self populateDefaultsPreflight]; [self populateDictionaryValue:dic ignorePrivateMessages:ignorePrivateMessages applyDefaults:YES bypassIsCopyCheck:NO]; [self populateDefaultsPostflight]; [self initializedClassHealthCheck]; return self; } return nil; } - (void)initializedClassHealthCheck { if (self.initializedAsCopy) { return; } if (self->_proxyPort == 0) { self->_proxyPort = IRCConnectionDefaultProxyPort; } if ([self isMutable]) { return; } NSParameterAssert(self->_connectionName.length > 0); } + (instancetype)newConfigByMerging:(IRCClientConfig *)config1 with:(IRCClientConfig *)config2 { NSParameterAssert(config1 != nil); NSParameterAssert(config2 != nil); IRCClientConfigMutable *config1Mutable = [config1 mutableCopy]; [config1Mutable populateDictionaryValue:config2.dictionaryValue ignorePrivateMessages:NO applyDefaults:NO bypassIsCopyCheck:YES]; if ([self isMutable]) { return config1Mutable; } return [config1Mutable copy]; } + (instancetype)newConfigWithNetwork:(IRCNetwork *)network { NSParameterAssert(network != nil); IRCClientConfigMutable *configMutable = [IRCClientConfigMutable new]; configMutable.connectionName = network.networkName; IRCServerMutable *server = [IRCServerMutable new]; server.serverAddress = network.serverAddress; server.serverPort = network.serverPort; server.prefersSecuredConnection = network.prefersSecuredConnection; configMutable.serverList = @[[server copy]]; if ([self isMutable]) { return configMutable; } return [configMutable copy]; } - (void)populateDictionaryValue:(NSDictionary *)dic ignorePrivateMessages:(BOOL)ignorePrivateMessages applyDefaults:(BOOL)applyDefaults bypassIsCopyCheck:(BOOL)bypassIsCopyCheck { NSParameterAssert(dic != nil); NSMutableDictionary *defaultsMutable = nil; if (applyDefaults) { defaultsMutable = [self->_defaults mutableCopy]; [defaultsMutable addEntriesFromDictionary:dic]; } else { defaultsMutable = [dic mutableCopy]; } /* Load the newest set of keys. */ [defaultsMutable assignUnsignedIntegerTo:&self->_dictionaryVersion forKey:@"dictionaryVersion"]; [defaultsMutable assignArrayTo:&self->_alternateNicknames forKey:@"alternateNicknames"]; [defaultsMutable assignArrayTo:&self->_loginCommands forKey:@"onConnectCommands"]; [defaultsMutable assignBoolTo:&self->_autoConnect forKey:@"autoConnect"]; [defaultsMutable assignBoolTo:&self->_autoReconnect forKey:@"autoReconnect"]; [defaultsMutable assignBoolTo:&self->_autoSleepModeDisconnect forKey:@"autoSleepModeDisconnect"]; [defaultsMutable assignBoolTo:&self->_autojoinWaitsForNickServ forKey:@"autojoinWaitsForNickServ"]; [defaultsMutable assignBoolTo:&self->_hideAutojoinDelayedWarnings forKey:@"hideAutojoinDelayedWarnings"]; [defaultsMutable assignBoolTo:&self->_hideNetworkUnavailabilityNotices forKey:@"hideNetworkUnavailabilityNotices"]; [defaultsMutable assignBoolTo:&self->_performDisconnectOnPongTimer forKey:@"performDisconnectOnPongTimer"]; [defaultsMutable assignBoolTo:&self->_performDisconnectOnReachabilityChange forKey:@"performDisconnectOnReachabilityChange"]; [defaultsMutable assignBoolTo:&self->_performPongTimer forKey:@"performPongTimer"]; [defaultsMutable assignBoolTo:&self->_prefersSecuredConnection forKey:@"prefersSecuredConnection"]; [defaultsMutable assignBoolTo:&self->_saslAuthenticationDisableExternalMechanism forKey:@"saslAuthenticationDisableExternalMechanism"]; [defaultsMutable assignBoolTo:&self->_sendAuthenticationRequestsToUserServ forKey:@"sendAuthenticationRequestsToUserServ"]; [defaultsMutable assignBoolTo:&self->_sendWhoCommandRequestsToChannels forKey:@"sendWhoCommandRequestsToChannels"]; [defaultsMutable assignBoolTo:&self->_setInvisibleModeOnConnect forKey:@"setInvisibleModeOnConnect"]; [defaultsMutable assignBoolTo:&self->_sidebarItemExpanded forKey:@"sidebarItemExpanded"]; [defaultsMutable assignBoolTo:&self->_validateServerCertificateChain forKey:@"validateServerCertificateChain"]; [defaultsMutable assignBoolTo:&self->_zncIgnoreConfiguredAutojoin forKey:@"zncIgnoreConfiguredAutojoin"]; [defaultsMutable assignBoolTo:&self->_zncIgnorePlaybackNotifications forKey:@"zncIgnorePlaybackNotifications"]; [defaultsMutable assignBoolTo:&self->_zncIgnoreUserNotifications forKey:@"zncIgnoreUserNotifications"]; [defaultsMutable assignBoolTo:&self->_zncOnlyPlaybackLatest forKey:@"zncOnlyPlaybackLatest"]; [defaultsMutable assignDoubleTo:&self->_lastMessageServerTime forKey:@"cachedLastServerTimeCapabilityReceivedAtTimestamp"]; [defaultsMutable assignObjectTo:&self->_identityClientSideCertificate forKey:@"identityClientSideCertificate"]; [defaultsMutable assignStringTo:&self->_awayNickname forKey:@"awayNickname"]; [defaultsMutable assignStringTo:&self->_connectionName forKey:@"connectionName"]; [defaultsMutable assignStringTo:&self->_nickname forKey:@"nickname"]; [defaultsMutable assignStringTo:&self->_normalLeavingComment forKey:@"normalLeavingComment"]; [defaultsMutable assignStringTo:&self->_proxyAddress forKey:@"proxyAddress"]; [defaultsMutable assignStringTo:&self->_proxyUsername forKey:@"proxyUsername"]; [defaultsMutable assignStringTo:&self->_realName forKey:@"realName"]; [defaultsMutable assignStringTo:&self->_serverAddress forKey:@"serverAddress"]; [defaultsMutable assignStringTo:&self->_sleepModeLeavingComment forKey:@"sleepModeLeavingComment"]; [defaultsMutable assignStringTo:&self->_uniqueIdentifier forKey:@"uniqueIdentifier"]; [defaultsMutable assignStringTo:&self->_username forKey:@"username"]; [defaultsMutable assignUnsignedIntegerTo:&self->_addressType forKey:@"addressType"]; [defaultsMutable assignUnsignedIntegerTo:&self->_cipherSuites forKey:@"cipherSuites"]; [defaultsMutable assignUnsignedIntegerTo:&self->_fallbackEncoding forKey:@"fallbackEncoding"]; [defaultsMutable assignUnsignedIntegerTo:&self->_floodControlDelayTimerInterval forKey:@"floodControlDelayTimerInterval"]; [defaultsMutable assignUnsignedIntegerTo:&self->_floodControlMaximumMessages forKey:@"floodControlMaximumMessages"]; [defaultsMutable assignUnsignedIntegerTo:&self->_primaryEncoding forKey:@"primaryEncoding"]; [defaultsMutable assignUnsignedIntegerTo:&self->_proxyType forKey:@"proxyType"]; [defaultsMutable assignUnsignedShortTo:&self->_proxyPort forKey:@"proxyPort"]; [defaultsMutable assignUnsignedShortTo:&self->_serverPort forKey:@"serverPort"]; /* -connectionPrefersIPv4 is a special exception to legacy support. We still load its value, regardless of dictionary version, so that we can show a user a warning when -connectionPrefersIPv4 == YES and -addressType == IPv6 (which is automatically migrates to). We then change value to NO when user modifies -addressType. */ [defaultsMutable assignBoolTo:&self->_connectionPrefersIPv4 forKey:@"connectionPrefersIPv4"]; /* If this is a copy operation, then we can just stop here. The rest of the data processed below, such as other configurations and backwards keys are already taken care of. */ if (self.initializedAsCopy && bypassIsCopyCheck == NO) { return; } /* Channel list */ NSMutableArray *channelListOut = [NSMutableArray array]; NSArray *channelListIn = [defaultsMutable arrayForKey:@"channelList"]; for (NSDictionary *e in channelListIn) { IRCChannelConfig *c = [[IRCChannelConfig alloc] initWithDictionary:e]; if (c.type == IRCChannelTypePrivateMessage) { if (ignorePrivateMessages == NO) { [channelListOut addObject:c]; } } else { [channelListOut addObject:c]; } } self->_channelList = [channelListOut copy]; /* Ignore list */ NSMutableArray *ignoreListOut = [NSMutableArray array]; NSArray *ignoreListIn = [defaultsMutable arrayForKey:@"ignoreList"]; for (NSDictionary *e in ignoreListIn) { IRCAddressBookEntry *c = [[IRCAddressBookEntry alloc] initWithDictionary:e]; [ignoreListOut addObject:c]; } self->_ignoreList = [ignoreListOut copy]; /* Highlight list */ NSMutableArray *highlightListOut = [NSMutableArray array]; NSArray *highlightListIn = [defaultsMutable arrayForKey:@"highlightList"]; for (NSDictionary *e in highlightListIn) { IRCHighlightMatchCondition *c = [[IRCHighlightMatchCondition alloc] initWithDictionary:e]; [highlightListOut addObject:c]; } self->_highlightList = [highlightListOut copy]; /* Server List */ NSMutableArray *serverListOut = [NSMutableArray array]; NSArray *serverListIn = [defaultsMutable arrayForKey:@"serverList"]; for (NSDictionary *e in serverListIn) { IRCServer *c = [[IRCServer alloc] initWithDictionary:e]; [serverListOut addObject:c]; } self->_serverList = [serverListOut copy]; /* Perform migration */ /* If legacy keys were assigned before new keys, then a transition would not occur properly. */ /* Since the new keys will read from -defaults if they are not present in /dic/, then those would override legacy keys when performing a first pass. */ /* Is everything up to date? */ NSUInteger dictionaryVersion = self->_dictionaryVersion; if (dictionaryVersion == IRCClientConfigDictionaryVersionLatest) { return; } /* 710 is latest version which means we migrate it at all times */ [self _migrate710Dictionary:dic withDefaults:defaultsMutable]; /* 704 is no longer the latest version but there was no version before it which means we only have to migrate it when the version is unknown. */ if (dictionaryVersion == 0) { [self _migrate704Dictionary:dic withDefaults:defaultsMutable]; } /* Update version */ self->_dictionaryVersion = IRCClientConfigDictionaryVersionLatest; } - (void)_migrate710Dictionary:(NSDictionary *)dic withDefaults:(NSMutableDictionary *)defaultsMutable { NSParameterAssert(dic != nil); NSParameterAssert(defaultsMutable != nil); TEXTUAL_IGNORE_DEPRECATION_BEGIN if (self.connectionPrefersIPv4) { TEXTUAL_IGNORE_DEPRECATION_END self->_addressType = IRCConnectionAddressTypeIPv4; } } - (void)_migrate704Dictionary:(NSDictionary *)dic withDefaults:(NSMutableDictionary *)defaultsMutable { NSParameterAssert(dic != nil); NSParameterAssert(defaultsMutable != nil); [defaultsMutable assignArrayTo:&self->_alternateNicknames forKey:@"identityAlternateNicknames"]; [defaultsMutable assignBoolTo:&self->_autoConnect forKey:@"connectOnLaunch"]; [defaultsMutable assignBoolTo:&self->_autoReconnect forKey:@"connectOnDisconnect"]; [defaultsMutable assignBoolTo:&self->_autoSleepModeDisconnect forKey:@"disconnectOnSleepMode"]; [defaultsMutable assignBoolTo:&self->_autojoinWaitsForNickServ forKey:@"autojoinWaitsForNickServIdentification"]; [defaultsMutable assignBoolTo:&self->_prefersSecuredConnection forKey:@"connectUsingSSL"]; [defaultsMutable assignBoolTo:&self->_setInvisibleModeOnConnect forKey:@"setInvisibleOnConnect"]; [defaultsMutable assignBoolTo:&self->_sidebarItemExpanded forKey:@"serverListItemIsExpanded"]; [defaultsMutable assignBoolTo:&self->_validateServerCertificateChain forKey:@"validateServerSideSSLCertificate"]; [defaultsMutable assignObjectTo:&self->_identityClientSideCertificate forKey:@"IdentitySSLCertificate"]; [defaultsMutable assignStringTo:&self->_awayNickname forKey:@"identityAwayNickname"]; [defaultsMutable assignStringTo:&self->_nickname forKey:@"identityNickname"]; [defaultsMutable assignStringTo:&self->_normalLeavingComment forKey:@"connectionDisconnectDefaultMessage"]; [defaultsMutable assignStringTo:&self->_proxyAddress forKey:@"proxyServerAddress"]; [defaultsMutable assignStringTo:&self->_proxyUsername forKey:@"proxyServerUsername"]; [defaultsMutable assignStringTo:&self->_realName forKey:@"identityRealname"]; [defaultsMutable assignStringTo:&self->_sleepModeLeavingComment forKey:@"connectionDisconnectSleepModeMessage"]; [defaultsMutable assignStringTo:&self->_username forKey:@"identityUsername"]; [defaultsMutable assignUnsignedIntegerTo:&self->_primaryEncoding forKey:@"characterEncodingDefault"]; [defaultsMutable assignUnsignedIntegerTo:&self->_fallbackEncoding forKey:@"characterEncodingFallback"]; [defaultsMutable assignUnsignedIntegerTo:&self->_proxyType forKey:@"proxyServerType"]; [defaultsMutable assignUnsignedShortTo:&self->_proxyPort forKey:@"proxyServerPort"]; [defaultsMutable assignDoubleTo:&self->_lastMessageServerTime forKey:@"cachedLastServerTimeCapacityReceivedAtTimestamp"]; /* Flood control */ /* This is here to migrate to the new properties. Saving these values in this dictionary key is no longer preferred. */ BOOL floodControlSetToDisabled = NO; NSDictionary *floodControlDic = [defaultsMutable dictionaryForKey:@"floodControl"]; if (floodControlDic) { NSNumber *serviceEnabled = floodControlDic[@"serviceEnabled"]; if (serviceEnabled && serviceEnabled.boolValue == NO) { floodControlSetToDisabled = YES; } [floodControlDic assignUnsignedIntegerTo:&self->_floodControlDelayTimerInterval forKey:@"delayTimerInterval"]; [floodControlDic assignUnsignedIntegerTo:&self->_floodControlMaximumMessages forKey:@"maximumMessageCount"]; } if (floodControlSetToDisabled == NO) { NSNumber *floodControlEnabled = defaultsMutable[@"isOutgoingFloodControlEnabled"]; if (floodControlEnabled && floodControlEnabled.boolValue == NO) { floodControlSetToDisabled = YES; } } /* An option to disable flood control no longer exists. If the user had flood control disabled when the option did exist, then set the the current values to appear disabled. */ if (floodControlSetToDisabled) { self->_floodControlDelayTimerInterval = IRCConnectionConfigFloodControlMinimumDelayInterval; self->_floodControlMaximumMessages = IRCConnectionConfigFloodControlMaximumMessageCount; } /* Migrate to keychain. */ NSString *proxyPassword = [defaultsMutable stringForKey:@"proxyServerPassword"]; if (proxyPassword) { self->_proxyPassword = [proxyPassword copy]; [self writeProxyPasswordToKeychain]; } /* Cipher suites */ /* The dictionary excludes defaults which means we need to be cautious about reading the value of dic when performing migration. */ if (dic[@"cipherSuites"] == nil) { NSNumber *connectionPrefersModernCiphers = dic[@"connectionPrefersModernCiphers"]; if (connectionPrefersModernCiphers && connectionPrefersModernCiphers.boolValue == NO) { self->_cipherSuites = RCMCipherSuiteCollectionNone; } } /* Migrate servers */ [self _migrateDictionaryToServerListV1Layout:defaultsMutable]; } - (void)_migrateDictionaryToServerListV1Layout:(NSDictionary *)dic { NSParameterAssert(dic != nil); /* This key is no longer assigned. We still check it so that clients that did not set dictionaryVersion but did set this key wont trigger migration again. */ id migratedToServerListV1Layout = dic[@"migratedToServerListV1Layout"]; if (migratedToServerListV1Layout && [migratedToServerListV1Layout boolValue]) { return; } /* Do not perform migration if already one server exists. */ /* IRCClientConfig inserts these values back into the exported dictionary for backwards compatibility which means once we imported them and have at least one server, then importing again will not help. */ if (self.serverList.count > 0) { return; } /* Perform migration */ NSString *serverAddress = [dic stringForKey:@"serverAddress"]; if (serverAddress.isValidInternetAddress == NO) { LogToConsoleDebug("Migration cancelled because of bad server address"); return; } uint16_t serverPort = [dic unsignedShortForKey:@"serverPort"]; if (serverPort == 0 || serverPort > TXMaximumTCPPort) { LogToConsoleDebug("Migration cancelled because of bad server port"); return; } BOOL prefersSecuredConnection = [dic boolForKey:@"prefersSecuredConnection"]; NSString *serverPasswordServiceName = [NSString stringWithFormat:@"textual.server.%@", self.uniqueIdentifier]; NSString *serverPassword = [XRKeychain getPasswordFromKeychainItem:@"Textual (Server Password)" withItemKind:@"application password" forUsername:nil serviceName:serverPasswordServiceName]; IRCServerMutable *server = [IRCServerMutable new]; server.serverAddress = serverAddress; server.serverPort = serverPort; server.serverPassword = serverPassword; server.prefersSecuredConnection = prefersSecuredConnection; [server writeServerPasswordToKeychain]; self->_serverList = @[[server copy]]; self->_migratedServerPasswordPendingDestroy = YES; } - (BOOL)isEqual:(id)object { if (object == nil) { return NO; } if (object == self) { return YES; } if ([object isKindOfClass:[IRCClientConfig class]] == NO) { return NO; } IRCClientConfig *objectCast = (IRCClientConfig *)object; NSDictionary *s1 = self.dictionaryValue; NSDictionary *s2 = objectCast.dictionaryValue; return ([s1 isEqualToDictionary:s2] && ((self->_nicknamePassword == nil && objectCast->_nicknamePassword == nil) || [self->_nicknamePassword isEqualToString:objectCast->_nicknamePassword]) && ((self->_proxyPassword == nil && objectCast->_proxyPassword == nil) || [self->_proxyPassword isEqualToString:objectCast->_proxyPassword])); } - (NSUInteger)hash { return self.uniqueIdentifier.hash; } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { IRCClientConfig *config = [self allocForCopyAsMutable:mutableCopy]; config->_nicknamePassword = self->_nicknamePassword; config->_proxyPassword = self->_proxyPassword; config->_defaults = self->_defaults; config->_migratedServerPasswordPendingDestroy = self->_migratedServerPasswordPendingDestroy; if (uniquing) { NSMutableArray *channelList = [self.channelList mutableCopy]; NSMutableArray *highlightList = [self.highlightList mutableCopy]; NSMutableArray *ignoreList = [self.ignoreList mutableCopy]; NSMutableArray *serverList = [self.serverList mutableCopy]; [channelList performSelectorOnObjectValueAndReplace:@selector(uniqueCopy)]; [highlightList performSelectorOnObjectValueAndReplace:@selector(uniqueCopy)]; [ignoreList performSelectorOnObjectValueAndReplace:@selector(uniqueCopy)]; [serverList performSelectorOnObjectValueAndReplace:@selector(uniqueCopy)]; config->_channelList = [channelList copy]; config->_highlightList = [highlightList copy]; config->_ignoreList = [ignoreList copy]; config->_serverList = [serverList copy]; config->_uniqueIdentifier = [NSString stringWithUUID]; } else // uniquing { config->_channelList = self->_channelList; config->_highlightList = self->_highlightList; config->_ignoreList = self->_ignoreList; config->_serverList = self->_serverList; } return [config initWithDictionary:self.dictionaryValueForCopy ignorePrivateMessages:NO]; } - (__kindof XRPortablePropertyDict *)mutableClass { return [IRCClientConfigMutable self]; } - (NSDictionary *)dictionaryValueForTarget:(XRPortablePropertyDictTarget)target { NSMutableDictionary *dic = [NSMutableDictionary dictionary]; [dic setUnsignedInteger:self->_dictionaryVersion forKey:@"dictionaryVersion"]; [dic maybeSetObject:self.alternateNicknames forKey:@"alternateNicknames"]; [dic maybeSetObject:self.awayNickname forKey:@"awayNickname"]; [dic maybeSetObject:self.connectionName forKey:@"connectionName"]; [dic maybeSetObject:self.loginCommands forKey:@"onConnectCommands"]; [dic maybeSetObject:self.nickname forKey:@"nickname"]; [dic maybeSetObject:self.normalLeavingComment forKey:@"normalLeavingComment"]; [dic maybeSetObject:self.proxyAddress forKey:@"proxyAddress"]; [dic maybeSetObject:self.proxyUsername forKey:@"proxyUsername"]; [dic maybeSetObject:self.realName forKey:@"realName"]; [dic maybeSetObject:self.sleepModeLeavingComment forKey:@"sleepModeLeavingComment"]; [dic maybeSetObject:self.uniqueIdentifier forKey:@"uniqueIdentifier"]; [dic maybeSetObject:self.username forKey:@"username"]; [dic setBool:self.autoConnect forKey:@"autoConnect"]; [dic setBool:self.autoReconnect forKey:@"autoReconnect"]; [dic setBool:self.autoSleepModeDisconnect forKey:@"autoSleepModeDisconnect"]; [dic setBool:self.autojoinWaitsForNickServ forKey:@"autojoinWaitsForNickServ"]; [dic setBool:self.hideAutojoinDelayedWarnings forKey:@"hideAutojoinDelayedWarnings"]; [dic setBool:self.hideNetworkUnavailabilityNotices forKey:@"hideNetworkUnavailabilityNotices"]; [dic setBool:self.performDisconnectOnPongTimer forKey:@"performDisconnectOnPongTimer"]; [dic setBool:self.performDisconnectOnReachabilityChange forKey:@"performDisconnectOnReachabilityChange"]; [dic setBool:self.performPongTimer forKey:@"performPongTimer"]; [dic setBool:self.saslAuthenticationDisableExternalMechanism forKey:@"saslAuthenticationDisableExternalMechanism"]; [dic setBool:self.sendAuthenticationRequestsToUserServ forKey:@"sendAuthenticationRequestsToUserServ"]; [dic setBool:self.sendWhoCommandRequestsToChannels forKey:@"sendWhoCommandRequestsToChannels"]; [dic setBool:self.setInvisibleModeOnConnect forKey:@"setInvisibleModeOnConnect"]; [dic setBool:self.validateServerCertificateChain forKey:@"validateServerCertificateChain"]; [dic setBool:self.zncIgnoreConfiguredAutojoin forKey:@"zncIgnoreConfiguredAutojoin"]; [dic setBool:self.zncIgnorePlaybackNotifications forKey:@"zncIgnorePlaybackNotifications"]; [dic setBool:self.zncIgnoreUserNotifications forKey:@"zncIgnoreUserNotifications"]; [dic setBool:self.zncOnlyPlaybackLatest forKey:@"zncOnlyPlaybackLatest"]; [dic setUnsignedInteger:self.addressType forKey:@"addressType"]; [dic setUnsignedInteger:self.cipherSuites forKey:@"cipherSuites"]; [dic setUnsignedInteger:self.fallbackEncoding forKey:@"fallbackEncoding"]; [dic setUnsignedInteger:self.floodControlDelayTimerInterval forKey:@"floodControlDelayTimerInterval"]; [dic setUnsignedInteger:self.floodControlMaximumMessages forKey:@"floodControlMaximumMessages"]; [dic setUnsignedInteger:self.primaryEncoding forKey:@"primaryEncoding"]; [dic setUnsignedInteger:self.proxyType forKey:@"proxyType"]; [dic setUnsignedShort:self.proxyPort forKey:@"proxyPort"]; /* These are items that cannot be synced over iCloud because they access data specific to this device or only contain state information which is not useful to other devices. */ [dic maybeSetObject:self.identityClientSideCertificate forKey:@"identityClientSideCertificate"]; [dic setBool:self.sidebarItemExpanded forKey:@"sidebarItemExpanded"]; [dic setDouble:self.lastMessageServerTime forKey:@"cachedLastServerTimeCapabilityReceivedAtTimestamp"]; /* Deprecated */ /* These values are inserted here for backwards compatibility with earlier versions of Textual */ TEXTUAL_IGNORE_DEPRECATION_BEGIN [dic setBool:self.connectionPrefersIPv4 forKey:@"connectionPrefersIPv4"]; TEXTUAL_IGNORE_DEPRECATION_END [dic setBool:self.legacyConnectionPrefersModernCiphers forKey:@"connectionPrefersModernCiphers"]; [dic maybeSetObject:self.legacyServerAddress forKey:@"serverAddress"]; [dic setBool:self.legacyPrefersSecuredConnection forKey:@"prefersSecuredConnection"]; [dic setUnsignedShort:self.legacyServerPort forKey:@"serverPort"]; /* During a copy operation, it is faster to copy these arrays as a whole. It also preserves -secretKey value in IRCChannelConfig since that will be lost when reconstructing from dictionary value. */ if (target == XRPortablePropertyDictTargetCopy || target == XRPortablePropertyDictTargetMutableCopy) { return [dic copy]; } /* Channel List */ NSMutableArray *channelListOut = [NSMutableArray array]; for (IRCChannelConfig *e in self.channelList) { NSDictionary *d = e.dictionaryValue; [channelListOut addObject:d]; } if (channelListOut.count > 0) { dic[@"channelList"] = [channelListOut copy]; } /* Highlight list */ NSMutableArray *highlightListOut = [NSMutableArray array]; for (IRCHighlightMatchCondition *e in self.highlightList) { NSDictionary *d = e.dictionaryValue; [highlightListOut addObject:d]; } if (highlightListOut.count > 0) { dic[@"highlightList"] = [highlightListOut copy]; } /* Ignore list */ NSMutableArray *ignoreListOut = [NSMutableArray array]; for (IRCAddressBookEntry *e in self.ignoreList) { NSDictionary *d = e.dictionaryValue; [ignoreListOut addObject:d]; } if (ignoreListOut.count > 0) { dic[@"ignoreList"] = [ignoreListOut copy]; } /* Servers */ NSMutableArray *serverListOut = [NSMutableArray array]; for (IRCServer *e in self.serverList) { NSDictionary *d = e.dictionaryValue; [serverListOut addObject:d]; } if (serverListOut.count > 0) { dic[@"serverList"] = [serverListOut copy]; } return [dic dictionaryByRemovingDefaults:self->_defaults allowEmptyValues:YES]; } #pragma mark - #pragma mark Keychain Management - (nullable NSString *)nicknamePassword { if (self->_nicknamePassword) { return self->_nicknamePassword; } return self.nicknamePasswordFromKeychain; } - (nullable NSString *)nicknamePasswordFromKeychain { NSString *nicknamePasswordServiceName = [NSString stringWithFormat:@"textual.nickserv.%@", self.uniqueIdentifier]; NSString *kcPassword = [XRKeychain getPasswordFromKeychainItem:@"Textual (NickServ)" withItemKind:@"application password" forUsername:nil serviceName:nicknamePasswordServiceName]; return kcPassword; } - (nullable NSString *)proxyPassword { if (self->_proxyPassword) { return self->_proxyPassword; } return self.proxyPasswordFromKeychain; } - (nullable NSString *)proxyPasswordFromKeychain { NSString *proxyPasswordServiceName = [NSString stringWithFormat:@"textual.proxy-server.%@", self.uniqueIdentifier]; NSString *kcPassword = [XRKeychain getPasswordFromKeychainItem:@"Textual (Proxy Server Password)" withItemKind:@"application password" forUsername:nil serviceName:proxyPasswordServiceName]; return kcPassword; } - (void)writeNicknamePasswordToKeychain { if (self->_nicknamePassword == nil) { return; } NSString *nicknamePasswordServiceName = [NSString stringWithFormat:@"textual.nickserv.%@", self.uniqueIdentifier]; [XRKeychain modifyOrAddKeychainItem:@"Textual (NickServ)" withItemKind:@"application password" forUsername:nil withNewPassword:self->_nicknamePassword serviceName:nicknamePasswordServiceName]; self->_nicknamePassword = nil; } - (void)writeProxyPasswordToKeychain { if (self->_proxyPassword == nil) { return; } NSString *proxyPasswordServiceName = [NSString stringWithFormat:@"textual.proxy-server.%@", self.uniqueIdentifier]; [XRKeychain modifyOrAddKeychainItem:@"Textual (Proxy Server Password)" withItemKind:@"application password" forUsername:nil withNewPassword:self->_proxyPassword serviceName:proxyPasswordServiceName]; self->_proxyPassword = nil; } - (void)destroyNicknamePasswordKeychainItem { NSString *nicknamePasswordServiceName = [NSString stringWithFormat:@"textual.nickserv.%@", self.uniqueIdentifier]; [XRKeychain deleteKeychainItem:@"Textual (NickServ)" withItemKind:@"application password" forUsername:nil serviceName:nicknamePasswordServiceName]; self->_nicknamePassword = nil; } - (void)destroyProxyPasswordKeychainItem { NSString *proxyPasswordServiceName = [NSString stringWithFormat:@"textual.proxy-server.%@", self.uniqueIdentifier]; [XRKeychain deleteKeychainItem:@"Textual (Proxy Server Password)" withItemKind:@"application password" forUsername:nil serviceName:proxyPasswordServiceName]; self->_proxyPassword = nil; } - (void)destroyServerPasswordKeychainItemAfterMigration { if (self->_migratedServerPasswordPendingDestroy == NO) { return; } self->_migratedServerPasswordPendingDestroy = NO; NSString *serverPasswordServiceName = [NSString stringWithFormat:@"textual.server.%@", self.uniqueIdentifier]; [XRKeychain deleteKeychainItem:@"Textual (Server Password)" withItemKind:@"application password" forUsername:nil serviceName:serverPasswordServiceName]; } #pragma mark - #pragma mark Deprecated Properties - (nullable NSString *)legacyServerAddress { IRCServer *server = self.serverList.firstObject; if (server == nil) { return self->_serverAddress; } return server.serverAddress; } - (uint16_t)legacyServerPort { IRCServer *server = self.serverList.firstObject; if (server == nil) { return self->_serverPort; } return server.serverPort; } - (BOOL)legacyPrefersSecuredConnection { IRCServer *server = self.serverList.firstObject; if (server == nil) { return self->_prefersSecuredConnection; } return server.prefersSecuredConnection; } - (BOOL)legacyConnectionPrefersModernCiphers { return (self.cipherSuites != RCMCipherSuiteCollectionNone); } - (BOOL)showConnectionPrefersIPv4Warning { TEXTUAL_IGNORE_DEPRECATION_BEGIN return (self.addressType == IRCConnectionAddressTypeIPv4 && self.connectionPrefersIPv4); TEXTUAL_IGNORE_DEPRECATION_END } @end #pragma mark - @implementation IRCClientConfigMutable @dynamic addressType; @dynamic alternateNicknames; @dynamic autoConnect; @dynamic autoReconnect; @dynamic autoSleepModeDisconnect; @dynamic autojoinWaitsForNickServ; @dynamic awayNickname; @dynamic channelList; @dynamic cipherSuites; @dynamic connectionName; @dynamic connectionPrefersIPv4; @dynamic fallbackEncoding; @dynamic floodControlDelayTimerInterval; @dynamic floodControlMaximumMessages; @dynamic hideAutojoinDelayedWarnings; @dynamic hideNetworkUnavailabilityNotices; @dynamic highlightList; @dynamic identityClientSideCertificate; @dynamic ignoreList; @dynamic lastMessageServerTime; @dynamic loginCommands; @dynamic nickname; @dynamic nicknamePassword; @dynamic normalLeavingComment; @dynamic performDisconnectOnPongTimer; @dynamic performDisconnectOnReachabilityChange; @dynamic performPongTimer; @dynamic primaryEncoding; @dynamic proxyAddress; @dynamic proxyPassword; @dynamic proxyPort; @dynamic proxyType; @dynamic proxyUsername; @dynamic realName; @dynamic saslAuthenticationDisableExternalMechanism; @dynamic sendAuthenticationRequestsToUserServ; @dynamic sendWhoCommandRequestsToChannels; @dynamic serverList; @dynamic setInvisibleModeOnConnect; @dynamic sidebarItemExpanded; @dynamic sleepModeLeavingComment; @dynamic username; @dynamic validateServerCertificateChain; @dynamic zncIgnoreConfiguredAutojoin; @dynamic zncIgnorePlaybackNotifications; @dynamic zncIgnoreUserNotifications; @dynamic zncOnlyPlaybackLatest; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyDict *)immutableClass { return [IRCClientConfig self]; } - (void)setAutoConnect:(BOOL)autoConnect { if (self->_autoConnect != autoConnect) { self->_autoConnect = autoConnect; } } - (void)setAutoReconnect:(BOOL)autoReconnect { if (self->_autoReconnect != autoReconnect) { self->_autoReconnect = autoReconnect; } } - (void)setAutoSleepModeDisconnect:(BOOL)autoSleepModeDisconnect { if (self->_autoSleepModeDisconnect != autoSleepModeDisconnect) { self->_autoSleepModeDisconnect = autoSleepModeDisconnect; } } - (void)setAutojoinWaitsForNickServ:(BOOL)autojoinWaitsForNickServ { if (self->_autojoinWaitsForNickServ != autojoinWaitsForNickServ) { self->_autojoinWaitsForNickServ = autojoinWaitsForNickServ; } } - (void)setConnectionPrefersIPv4:(BOOL)connectionPrefersIPv4 { TEXTUAL_DEPRECATED_WARNING if (self->_connectionPrefersIPv4 != connectionPrefersIPv4) { self->_connectionPrefersIPv4 = connectionPrefersIPv4; } } - (void)setHideAutojoinDelayedWarnings:(BOOL)hideAutojoinDelayedWarnings { if (self->_hideAutojoinDelayedWarnings != hideAutojoinDelayedWarnings) { self->_hideAutojoinDelayedWarnings = hideAutojoinDelayedWarnings; } } - (void)setHideNetworkUnavailabilityNotices:(BOOL)hideNetworkUnavailabilityNotices { if (self->_hideNetworkUnavailabilityNotices != hideNetworkUnavailabilityNotices) { self->_hideNetworkUnavailabilityNotices = hideNetworkUnavailabilityNotices; } } - (void)setPerformDisconnectOnPongTimer:(BOOL)performDisconnectOnPongTimer { if (self->_performDisconnectOnPongTimer != performDisconnectOnPongTimer) { self->_performDisconnectOnPongTimer = performDisconnectOnPongTimer; } } - (void)setPerformDisconnectOnReachabilityChange:(BOOL)performDisconnectOnReachabilityChange { if (self->_performDisconnectOnReachabilityChange != performDisconnectOnReachabilityChange) { self->_performDisconnectOnReachabilityChange = performDisconnectOnReachabilityChange; } } - (void)setPerformPongTimer:(BOOL)performPongTimer { if (self->_performPongTimer != performPongTimer) { self->_performPongTimer = performPongTimer; } } - (void)setSaslAuthenticationDisableExternalMechanism:(BOOL)saslAuthenticationDisableExternalMechanism { if (self->_saslAuthenticationDisableExternalMechanism != saslAuthenticationDisableExternalMechanism) { self->_saslAuthenticationDisableExternalMechanism = saslAuthenticationDisableExternalMechanism; } } - (void)setSendAuthenticationRequestsToUserServ:(BOOL)sendAuthenticationRequestsToUserServ { if (self->_sendAuthenticationRequestsToUserServ != sendAuthenticationRequestsToUserServ) { self->_sendAuthenticationRequestsToUserServ = sendAuthenticationRequestsToUserServ; } } - (void)setSendWhoCommandRequestsToChannels:(BOOL)sendWhoCommandRequestsToChannels { if (self->_sendWhoCommandRequestsToChannels != sendWhoCommandRequestsToChannels) { self->_sendWhoCommandRequestsToChannels = sendWhoCommandRequestsToChannels; } } - (void)setSetInvisibleModeOnConnect:(BOOL)setInvisibleModeOnConnect { if (self->_setInvisibleModeOnConnect != setInvisibleModeOnConnect) { self->_setInvisibleModeOnConnect = setInvisibleModeOnConnect; } } - (void)setSidebarItemExpanded:(BOOL)sidebarItemExpanded { if (self->_sidebarItemExpanded != sidebarItemExpanded) { self->_sidebarItemExpanded = sidebarItemExpanded; } } - (void)setValidateServerCertificateChain:(BOOL)validateServerCertificateChain { if (self->_validateServerCertificateChain != validateServerCertificateChain) { self->_validateServerCertificateChain = validateServerCertificateChain; } } - (void)setZncIgnoreConfiguredAutojoin:(BOOL)zncIgnoreConfiguredAutojoin { if (self->_zncIgnoreConfiguredAutojoin != zncIgnoreConfiguredAutojoin) { self->_zncIgnoreConfiguredAutojoin = zncIgnoreConfiguredAutojoin; } } - (void)setZncIgnorePlaybackNotifications:(BOOL)zncIgnorePlaybackNotifications { if (self->_zncIgnorePlaybackNotifications != zncIgnorePlaybackNotifications) { self->_zncIgnorePlaybackNotifications = zncIgnorePlaybackNotifications; } } - (void)setZncIgnoreUserNotifications:(BOOL)zncIgnoreUserNotifications { if (self->_zncIgnoreUserNotifications != zncIgnoreUserNotifications) { self->_zncIgnoreUserNotifications = zncIgnoreUserNotifications; } } - (void)setZncOnlyPlaybackLatest:(BOOL)zncOnlyPlaybackLatest { if (self->_zncOnlyPlaybackLatest != zncOnlyPlaybackLatest) { self->_zncOnlyPlaybackLatest = zncOnlyPlaybackLatest; } } - (void)setAddressType:(IRCConnectionAddressType)addressType { if (self->_addressType != addressType) { self->_addressType = addressType; } } - (void)setProxyType:(IRCConnectionProxyType)proxyType { if (self->_proxyType != proxyType) { self->_proxyType = proxyType; } } - (void)setIgnoreList:(NSArray *)ignoreList { NSParameterAssert(ignoreList != nil); if (self->_ignoreList != ignoreList) { self->_ignoreList = [ignoreList copy]; } } - (void)setChannelList:(NSArray *)channelList { NSParameterAssert(channelList != nil); if (self->_channelList != channelList) { self->_channelList = [channelList copy]; } } - (void)setHighlightList:(NSArray *)highlightList { NSParameterAssert(highlightList != nil); if (self->_highlightList != highlightList) { self->_highlightList = [highlightList copy]; } } - (void)setAlternateNicknames:(NSArray *)alternateNicknames { NSParameterAssert(alternateNicknames != nil); if (self->_alternateNicknames != alternateNicknames) { self->_alternateNicknames = [alternateNicknames copy]; } } - (void)setLoginCommands:(NSArray *)loginCommands { NSParameterAssert(loginCommands != nil); if (self->_loginCommands != loginCommands) { self->_loginCommands = [loginCommands copy]; } } - (void)setServerList:(NSArray *)serverList { NSParameterAssert(serverList != nil); if (self->_serverList != serverList) { self->_serverList = [serverList copy]; } } - (void)setIdentityClientSideCertificate:(nullable NSData *)identityClientSideCertificate { if (self->_identityClientSideCertificate != identityClientSideCertificate) { self->_identityClientSideCertificate = [identityClientSideCertificate copy]; } } - (void)setAwayNickname:(nullable NSString *)awayNickname { if (self->_awayNickname != awayNickname) { self->_awayNickname = [awayNickname copy]; } } - (void)setConnectionName:(NSString *)connectionName { NSParameterAssert(connectionName != nil); if (self->_connectionName != connectionName) { self->_connectionName = [connectionName copy]; } } - (void)setNickname:(NSString *)nickname { NSParameterAssert(nickname != nil); if (self->_nickname != nickname) { self->_nickname = [nickname copy]; } } - (void)setNicknamePassword:(nullable NSString *)nicknamePassword { if (self->_nicknamePassword != nicknamePassword) { self->_nicknamePassword = [nicknamePassword copy]; } } - (void)setNormalLeavingComment:(NSString *)normalLeavingComment { NSParameterAssert(normalLeavingComment != nil); if (self->_normalLeavingComment != normalLeavingComment) { self->_normalLeavingComment = [normalLeavingComment copy]; } } - (void)setProxyAddress:(nullable NSString *)proxyAddress { if (self->_proxyAddress != proxyAddress) { self->_proxyAddress = [proxyAddress copy]; } } - (void)setProxyPassword:(nullable NSString *)proxyPassword { if (self->_proxyPassword != proxyPassword) { self->_proxyPassword = [proxyPassword copy]; } } - (void)setProxyUsername:(nullable NSString *)proxyUsername { if (self->_proxyUsername != proxyUsername) { self->_proxyUsername = [proxyUsername copy]; } } - (void)setRealName:(NSString *)realName { NSParameterAssert(realName != nil); if (self->_realName != realName) { self->_realName = [realName copy]; } } - (void)setSleepModeLeavingComment:(NSString *)sleepModeLeavingComment { NSParameterAssert(sleepModeLeavingComment != nil); if (self->_sleepModeLeavingComment != sleepModeLeavingComment) { self->_sleepModeLeavingComment = [sleepModeLeavingComment copy]; } } - (void)setUsername:(NSString *)username { NSParameterAssert(username != nil); if (self->_username != username) { self->_username = [username copy]; } } - (void)setFallbackEncoding:(NSStringEncoding)fallbackEncoding { if (self->_fallbackEncoding != fallbackEncoding) { self->_fallbackEncoding = fallbackEncoding; } } - (void)setPrimaryEncoding:(NSStringEncoding)primaryEncoding { if (self->_primaryEncoding != primaryEncoding) { self->_primaryEncoding = primaryEncoding; } } - (void)setLastMessageServerTime:(NSTimeInterval)lastMessageServerTime { if (self->_lastMessageServerTime != lastMessageServerTime) { self->_lastMessageServerTime = lastMessageServerTime; } } - (void)setFloodControlDelayTimerInterval:(NSUInteger)floodControlDelayTimerInterval { NSParameterAssert(floodControlDelayTimerInterval >= IRCConnectionConfigFloodControlMinimumDelayInterval && floodControlDelayTimerInterval <= IRCConnectionConfigFloodControlMaximumDelayInterval); if (self->_floodControlDelayTimerInterval != floodControlDelayTimerInterval) { self->_floodControlDelayTimerInterval = floodControlDelayTimerInterval; } } - (void)setFloodControlMaximumMessages:(NSUInteger)floodControlMaximumMessages { NSParameterAssert(floodControlMaximumMessages >= IRCConnectionConfigFloodControlMinimumMessageCount && floodControlMaximumMessages <= IRCConnectionConfigFloodControlMaximumMessageCount); if (self->_floodControlMaximumMessages != floodControlMaximumMessages) { self->_floodControlMaximumMessages = floodControlMaximumMessages; } } - (void)setProxyPort:(uint16_t)proxyPort { if (self->_proxyPort != proxyPort) { self->_proxyPort = proxyPort; } } - (void)setCipherSuites:(RCMCipherSuiteCollection)cipherSuites { if (self->_cipherSuites != cipherSuites) { self->_cipherSuites = cipherSuites; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCClientRequestedCommands.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClientRequestedCommandsPrivate.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, IRCClientRequestedCommandVisibility) { IRCClientRequestedCommandVisibilityUnknown = 0, IRCClientRequestedCommandVisibilityHidden, IRCClientRequestedCommandVisibilityVisible }; @interface IRCClientRequestedCommand : NSObject @property (nonatomic, assign) IRCRemoteCommand command; @property (nonatomic, assign) BOOL hiddenResponse; @property (nonatomic, assign) BOOL enforceCount; @property (nonatomic, assign) NSUInteger count; @end @interface IRCClientRequestedCommands () /* We could use a dictionary or cache with command as key and an array of objects for the assigned object but that is much more complex than just scanning a one level array. */ @property (nonatomic, strong) NSMutableArray *invokedCommandsInt; @end @implementation IRCClientRequestedCommands - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.invokedCommandsInt = [NSMutableArray array]; } - (nullable IRCClientRequestedCommand *)findCommand:(IRCRemoteCommand)command { @synchronized (self.invokedCommandsInt) { return [self.invokedCommandsInt objectPassingTest:^BOOL(IRCClientRequestedCommand *object, NSUInteger index, BOOL *stop) { return (object.command == command); }]; } } - (void)addCommand:(IRCRemoteCommand)command hiddenResponse:(BOOL)hiddenResponse { [self addCommand:command withCount:0 hiddenResponse:hiddenResponse]; } - (void)addCommand:(IRCRemoteCommand)command withCount:(NSUInteger)count hiddenResponse:(BOOL)hiddenResponse { IRCClientRequestedCommand *commandObject = [IRCClientRequestedCommand new]; commandObject.command = command; commandObject.hiddenResponse = hiddenResponse; commandObject.enforceCount = (count > 0); commandObject.count = count; @synchronized (self.invokedCommandsInt) { [self.invokedCommandsInt addObject:commandObject]; } } - (void)removeCommands { @synchronized (self.invokedCommandsInt) { [self.invokedCommandsInt removeAllObjects]; } } - (void)removeCommand:(IRCRemoteCommand)command { /* There can be multiple commands with same context. When we remove, we remove the first that is matched and let next pass of responses remove the others. */ IRCClientRequestedCommand *commandObject = [self findCommand:command]; if (commandObject == nil) { return; } [self removeCommandObject:commandObject]; } - (void)removeCommandObject:(IRCClientRequestedCommand *)commandObject { NSParameterAssert(commandObject != nil); @synchronized (self.invokedCommandsInt) { [self.invokedCommandsInt removeObject:commandObject]; } } - (void)decrementCommandCount:(IRCRemoteCommand)command { IRCClientRequestedCommand *commandObject = [self findCommand:command]; if (commandObject == nil) { return; } if (commandObject.enforceCount == NO) { return; } commandObject.count -= 1; if (commandObject.count == 0) { [self removeCommandObject:commandObject]; } } - (IRCClientRequestedCommandVisibility)commandHiddenState:(IRCRemoteCommand)command { IRCClientRequestedCommand *commandObject = [self findCommand:command]; if (commandObject == nil) { return IRCClientRequestedCommandVisibilityUnknown; } if (commandObject.hiddenResponse == NO) { return IRCClientRequestedCommandVisibilityVisible; } else { return IRCClientRequestedCommandVisibilityHidden; } } @end #pragma mark - @implementation IRCClientRequestedCommands (Helpers) - (BOOL)inVisibleIsonRequest { return ([self commandHiddenState:IRCRemoteCommandIson] == IRCClientRequestedCommandVisibilityVisible); } - (void)recordIsonRequestOpened { [self addCommand:IRCRemoteCommandIson hiddenResponse:YES]; } - (void)recordIsonRequestOpenedAsVisible { [self addCommand:IRCRemoteCommandIson hiddenResponse:NO]; } - (void)recordIsonRequestClosed { [self removeCommand:IRCRemoteCommandIson]; } #if 0 - (BOOL)inVisibleMonitorRequest { return ([self commandHiddenState:IRCRemoteCommandMonitor] == IRCClientRequestedCommandVisibilityVisible); } - (void)recordMonitorRequestOpened { [self addCommand:IRCRemoteCommandMonitor hiddenResponse:YES]; } - (void)recordMonitorRequestOpenedWithCount:(NSUInteger)count { NSParameterAssert(count > 0); [self addCommand:IRCRemoteCommandMonitor withCount:count hiddenResponse:YES]; } - (void)recordMonitorRequestOpenedAsVisibleWithCount:(NSUInteger)count { NSParameterAssert(count > 0); [self addCommand:IRCRemoteCommandMonitor withCount:count hiddenResponse:NO]; } - (void)recordMonitorRequestOpenedAsVisible { [self addCommand:IRCRemoteCommandMonitor hiddenResponse:NO]; } - (void)recordMonitorRequestClosedOne { [self decrementCommandCount:IRCRemoteCommandMonitor]; } - (void)recordMonitorRequestClosed { [self removeCommand:IRCRemoteCommandMonitor]; } - (BOOL)inVisibleNamesRequest { return ([self commandHiddenState:IRCRemoteCommandNames] == IRCClientRequestedCommandVisibilityVisible); } - (void)recordNamesRequestOpened { [self addCommand:IRCRemoteCommandNames hiddenResponse:YES]; } - (void)recordNamesRequestOpenedAsVisible { [self addCommand:IRCRemoteCommandNames hiddenResponse:NO]; } - (void)recordNamesRequestClosed { [self removeCommand:IRCRemoteCommandNames]; } - (BOOL)inVisibleWatchRequest { return ([self commandHiddenState:IRCRemoteCommandWatch] == IRCClientRequestedCommandVisibilityVisible); } - (void)recordWatchRequestOpened { [self addCommand:IRCRemoteCommandWatch hiddenResponse:YES]; } - (void)recordWatchRequestOpenedWithCount:(NSUInteger)count { NSParameterAssert(count > 0); [self addCommand:IRCRemoteCommandWatch withCount:count hiddenResponse:YES]; } - (void)recordWatchRequestOpenedAsVisibleWithCount:(NSUInteger)count { NSParameterAssert(count > 0); [self addCommand:IRCRemoteCommandWatch withCount:count hiddenResponse:NO]; } - (void)recordWatchRequestOpenedAsVisible { [self addCommand:IRCRemoteCommandWatch hiddenResponse:NO]; } - (void)recordWatchRequestClosedOne { [self decrementCommandCount:IRCRemoteCommandWatch]; } - (void)recordWatchRequestClosed { [self removeCommand:IRCRemoteCommandWatch]; } #endif - (BOOL)inVisibleWhoRequest { return ([self commandHiddenState:IRCRemoteCommandWho] == IRCClientRequestedCommandVisibilityVisible); } - (void)recordWhoRequestOpened { [self addCommand:IRCRemoteCommandWho hiddenResponse:YES]; } - (void)recordWhoRequestOpenedAsVisible { [self addCommand:IRCRemoteCommandWho hiddenResponse:NO]; } - (void)recordWhoRequestClosed { [self removeCommand:IRCRemoteCommandWho]; } @end #pragma mark - @implementation IRCClientRequestedCommand @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCCommandIndex.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferencesLocal.h" #import "TPCResourceManager.h" #import "IRCCommandIndexPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _reservedSlotDictionaryKey @"Reserved Information" @implementation IRCCommandIndex static NSArray * _Nullable _cachedLocalCommandList = nil; static NSDictionary *IRCCommandIndexLocalData = nil; static NSDictionary *IRCCommandIndexRemoteData = nil; + (void)populateCommandIndex { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self _populateCommandIndex]; }); } + (void)_populateCommandIndex { /* Populate public data */ NSDictionary *publicValues = [TPCResourceManager dictionaryFromResources:@"IRCCommandIndexLocalData" cacheValue:NO]; if (publicValues) { NSMutableDictionary *publicValuesMutable = [publicValues mutableCopy]; [publicValuesMutable removeObjectForKey:_reservedSlotDictionaryKey]; IRCCommandIndexLocalData = [publicValuesMutable copy]; } /* Populate private data */ NSDictionary *privateValues = [TPCResourceManager dictionaryFromResources:@"IRCCommandIndexRemoteData" cacheValue:NO]; if (privateValues) { NSMutableDictionary *privateValuesMutable = [privateValues mutableCopy]; [privateValuesMutable removeObjectForKey:_reservedSlotDictionaryKey]; IRCCommandIndexRemoteData = [privateValuesMutable copy]; } /* Only error checking we need. It either fails or succeeds. */ NSParameterAssert(IRCCommandIndexRemoteData != nil); NSParameterAssert(IRCCommandIndexLocalData != nil); } + (void)invalidateCaches { _cachedLocalCommandList = nil; } + (void)rebuildLocalCommandList { NSMutableArray *commandList = [NSMutableArray array]; BOOL developerModeEnabled = [TPCPreferences developerModeEnabled]; [IRCCommandIndexLocalData enumerateKeysAndObjectsUsingBlock:^(NSString *indexKey, NSDictionary *indexValue, BOOL *stop) { BOOL developerOnly = [indexValue boolForKey:@"developerModeOnly"]; if (developerModeEnabled == NO && developerOnly) { return; } [commandList addObject:indexKey.uppercaseString]; }]; _cachedLocalCommandList = [commandList copy]; } + (NSArray *)localCommandList { if (_cachedLocalCommandList == nil) { [self rebuildLocalCommandList]; } return _cachedLocalCommandList; } + (NSUInteger)indexOfLocalCommand:(NSString *)command { return [self indexOfCommand:command isLocal:YES]; } + (NSUInteger)indexOfRemoteCommand:(NSString *)command { return [self indexOfCommand:command isLocal:NO]; } + (NSUInteger)indexOfCommand:(NSString *)command isLocal:(BOOL)isLocalCommand { NSDictionary *index = nil; if (isLocalCommand) { index = IRCCommandIndexLocalData[command.lowercaseString]; } else { index = IRCCommandIndexRemoteData[command.lowercaseString]; } if (index == nil) { return NSNotFound; } if (isLocalCommand) { if ([index boolForKey:@"developerModeOnly"] && [TPCPreferences developerModeEnabled] == NO) { return NSNotFound; } } return [index unsignedIntegerForKey:@"indexValue"]; } + (NSUInteger)colonPositionForRemoteCommand:(NSString *)command { NSParameterAssert(command != nil); NSDictionary *index = IRCCommandIndexRemoteData[command.lowercaseString]; if (index == nil) { return NSNotFound; } NSInteger position = [index integerForKey:@"outgoingColonIndex"]; if (position < 0) { return NSNotFound; } return position; } + (nullable NSString *)syntaxForLocalCommand:(NSString *)command { NSParameterAssert(command != nil); NSDictionary *index = IRCCommandIndexLocalData[command.lowercaseString]; if (index == nil) { return nil; } NSString *commandFormatted = command.uppercaseString; NSString *argumentFormat = index[@"arguments"]; if (argumentFormat) { return [NSString stringWithFormat:@"%@ %@", commandFormatted, argumentFormat]; } return commandFormatted; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCConnection.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "RCMConnectionManagerProtocol.h" #import #import "NSObjectHelperPrivate.h" #import "GCDAsyncSocketExtensions.h" #import "TLOLocalization.h" #import "TPCPreferencesLocal.h" #import "IRCClient.h" #import "IRCConnectionConfig.h" #import "IRCConnectionErrors.h" #import "IRCConnectionPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCConnection () @property (nonatomic, weak, readwrite) IRCClient *client; @property (nonatomic, strong) NSXPCConnection *serviceConnection; @property (nonatomic, strong, nullable) SFCertificateTrustPanel *trustPanel; @property (nonatomic, assign) BOOL trustPanelDoNotInvokeCompletionBlock; @property (nonatomic, assign) BOOL connectionInvalidatedVoluntarily; @property (nonatomic, copy, readwrite) NSString *uniqueIdentifier; @end @implementation IRCConnection #pragma mark - #pragma mark Initialization - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithConfig:(IRCConnectionConfig *)config onClient:(IRCClient *)client { NSParameterAssert(config != nil); NSParameterAssert(client != nil); if ((self = [super init])) { self.client = client; self.config = config; self.uniqueIdentifier = [NSString stringWithUUID]; } return self; } - (void)resetState { self.isConnecting = NO; self.isConnected = NO; self.isConnectedWithClientSideCertificate = NO; self.isDisconnecting = NO; self.EOFReceived = NO; self.isSecured = NO; self.isSending = NO; self.connectedAddress = nil; self.connectionInvalidatedVoluntarily = NO; } #pragma mark - #pragma mark Process Management - (void)invalidateProcess { if (self.serviceConnection == nil) { return; } LogToConsoleDebug("Invalidating process..."); [self.serviceConnection invalidate]; } - (void)warmProcessIfNeeded { if (self.serviceConnection != nil) { return; } LogToConsoleDebug("Warming process..."); [self warmProcess]; } - (void)warmProcess { NSXPCConnection *serviceConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.codeux.app-utilities.Textual-IRCConnectionHost"]; NSXPCInterface *remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(RCMConnectionManagerServerProtocol)]; serviceConnection.remoteObjectInterface = remoteObjectInterface; NSXPCInterface *exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(RCMConnectionManagerClientProtocol)]; serviceConnection.exportedInterface = exportedInterface; serviceConnection.exportedObject = self; serviceConnection.interruptionHandler = ^{ [self interruptionHandler]; LogToConsole("Interruption handler called"); }; serviceConnection.invalidationHandler = ^{ [self invalidationHandler]; LogToConsole("Invalidation handler called"); }; [serviceConnection resume]; self.serviceConnection = serviceConnection; } - (void)interruptionHandler { [self invalidateProcess]; } - (void)invalidationHandler { self.serviceConnection = nil; /* -ircConnectionDidDisconnectWithError: instructs the process to voluntarily invalidate, so if we reach here, then its pretty certain something big happened and we need to let the client know. */ if ((self.isConnecting || self.isConnected) && self.connectionInvalidatedVoluntarily == NO) { NSString *errorMessage = TXTLS(@"IRC[vdy-jk]"); NSError *error = [NSError errorWithDomain:IRCConnectionErrorDomain code:IRCConnectionErrorCodeOther userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; [self _ircConnectionDidDisconnectWithError:error]; } [self resetState]; } - (id )remoteObjectProxy { return [self remoteObjectProxyWithErrorHandler:nil]; } - (id )remoteObjectProxyWithErrorHandler:(void (^ _Nullable)(NSError *error))handler { return [self.serviceConnection remoteObjectProxyWithErrorHandler:^(NSError *error) { LogToConsoleError("Error occurred while communicating with service: %{public}@", error.localizedDescription); if (handler) { handler(error); } }]; } #pragma mark - #pragma mark Open/Close Connection - (void)open { if (self.isConnecting || self.isConnected || self.isDisconnecting) { return; } [self warmProcessIfNeeded]; self.isConnecting = YES; [[self remoteObjectProxy] openWithConfig:self.config]; if ([TPCPreferences appNapEnabled] == NO) { [[self remoteObjectProxy] disableAppNap]; } [[self remoteObjectProxy] disableSuddenTermination]; } - (void)close { if (self.isDisconnecting) { return; } if (self.isConnecting || self.isConnected) { /* Disconnect caused by calling -close on the service will cause -ircConnectionDidDisconnectWithError: to invoke -invalidateProcess for us, so don't call it on this condition. */ self.isDisconnecting = YES; [[self remoteObjectProxy] close]; } else { [self invalidateProcess]; } } #pragma mark - #pragma mark Utilities - (void)enforceFloodControl { if (self.isConnected == NO) { return; } [[self remoteObjectProxy] enforceFloodControl]; } - (void)openSecuredConnectionCertificateModal { [[self remoteObjectProxy] exportSecureConnectionInformation:^(NSString * _Nullable policyName, tls_protocol_version_t protocolType, tls_ciphersuite_t cipherSuites, NSArray *certificateChain) { if (policyName == nil) { return; } SecTrustRef trustRef = [RCMSecureTransport trustFromCertificateChain:certificateChain withPolicyName:policyName]; if (trustRef == NULL) { return; } NSString *protocolDescription = [RCMSecureTransport descriptionForProtocolType:protocolType]; NSString *cipherDescription = [RCMSecureTransport descriptionForCipherSuite:cipherSuites]; if (protocolDescription == nil || cipherDescription == nil) { CFRelease(trustRef); return; } NSString *protocolSummary = nil; if ([RCMSecureTransport isCipherSuiteDeprecated:cipherSuites] == NO) { protocolSummary = TXTLS(@"Prompts[2jq-t5]", protocolDescription, cipherDescription); } else { protocolSummary = TXTLS(@"Prompts[8ou-pu]", protocolDescription, cipherDescription); } NSString *defaultButtonTitle = TXTLS(@"Prompts[aqw-q1]"); NSString *alternateButtonTitle = nil; NSString *promptTitleText = TXTLS(@"Prompts[sfx-xx]", policyName); NSString *promptInformativeText = nil; if (protocolSummary == nil) { promptInformativeText = TXTLS(@"Prompts[ihy-mz]", policyName); } else { promptInformativeText = TXTLS(@"Prompts[iun-45]", policyName, protocolSummary); } __block NSWindow *window = nil; XRPerformBlockSynchronouslyOnMainQueue(^{ window = [NSApp keyWindow]; }); (void) [RCMTrustPanel presentTrustPanelInWindow:window body:promptInformativeText title:promptTitleText defaultButton:defaultButtonTitle alternateButton:alternateButtonTitle trustRef:trustRef completionBlock:^(SecTrustRef trustRef, BOOL trusted, id contextInfo) { CFRelease(trustRef); }]; }]; } - (void)openInsecureCertificateTrustPanel:(RCMTrustResponse)trustBlock { if (self.trustPanel != nil) { return; } [[self remoteObjectProxy] exportSecureConnectionInformation:^(NSString * _Nullable policyName, tls_protocol_version_t protocolType, tls_ciphersuite_t cipherSuites, NSArray *certificateChain) { if (policyName == nil) { return; } SecTrustRef trustRef = [RCMSecureTransport trustFromCertificateChain:certificateChain withPolicyName:policyName]; if (trustRef == NULL) { return; } NSString *defaultButtonTitle = TXTLS(@"Prompts[zjw-bd]"); NSString *alternateButtonTitle = TXTLS(@"Prompts[qso-2g]"); NSString *promptTitleText = TXTLS(@"Prompts[m8b-58]", policyName); NSString *promptInformativeText = TXTLS(@"Prompts[85z-qw]", policyName); __weak typeof(self) weakSelf = self; self.trustPanel = [RCMTrustPanel presentTrustPanelInWindow:nil body:promptInformativeText title:promptTitleText defaultButton:defaultButtonTitle alternateButton:alternateButtonTitle trustRef:trustRef completionBlock:^(SecTrustRef trustRef, BOOL trusted, id contextInfo) { CFRelease(trustRef); weakSelf.trustPanel = nil; if (weakSelf.trustPanelDoNotInvokeCompletionBlock) { weakSelf.trustPanelDoNotInvokeCompletionBlock = NO; return; } ((RCMTrustResponse)contextInfo)(trusted); } contextInfo:trustBlock]; }]; } - (void)closeInsecureCertificateTrustPanel { if (self.trustPanel == nil) { return; } SEL dismissSelector = NSSelectorFromString(@"_dismissWithCode:"); if ([self.trustPanel respondsToSelector:dismissSelector]) { self.trustPanelDoNotInvokeCompletionBlock = YES; NSMethodSignature *signature = [self.trustPanel methodSignatureForSelector:dismissSelector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self.trustPanel]; [invocation setSelector:dismissSelector]; NSModalResponse cancel = NSModalResponseCancel; [invocation setArgument:&cancel atIndex:2]; [invocation invoke]; } } #pragma mark - #pragma mark Encode Data - (nullable NSString *)convertFromCommonEncoding:(NSData *)data { return [self.client convertFromCommonEncoding:data]; } - (nullable NSData *)convertToCommonEncoding:(NSString *)data { return [self.client convertToCommonEncoding:data]; } #pragma mark - #pragma mark Send Data - (void)sendLine:(NSString *)line { NSParameterAssert(line != nil); line = [line stringByAppendingString:@"\x0d\x0a"]; NSData *dataToSend = [self convertToCommonEncoding:line]; if (dataToSend == nil) { return; } self.isSending = YES; /* PONG replies are extremely important. There is no reason they should be placed in the flood control queue. This writes them directly to the socket instead of actually waiting for the queue. We only need this check if we actually have flood control enabled. */ if ([line hasPrefix:@"PONG"]) { [[self remoteObjectProxy] sendData:dataToSend bypassQueue:YES]; return; } [[self remoteObjectProxy] sendData:dataToSend]; } - (void)clearSendQueue { [[self remoteObjectProxy] clearSendQueue]; } #pragma mark - #pragma mark Socket Delegate - (void)ircConnectionWillConnectToProxy:(NSString *)proxyHost port:(uint16_t)proxyPort { XRPerformBlockSynchronouslyOnMainQueue(^{ [self.client ircConnection:self willConnectToProxy:proxyHost port:proxyPort]; }); } - (void)ircConnectionDidConnectToHost:(nullable NSString *)host { self.connectedAddress = host; self.isConnecting = NO; self.isConnected = YES; XRPerformBlockSynchronouslyOnMainQueue(^{ [self.client ircConnectionDidConnect:self]; }); } - (void)ircConnectionDidSecureConnectionWithProtocolType:(tls_protocol_version_t)protocolType cipherSuite:(tls_ciphersuite_t)cipherSuite { self.isSecured = YES; if (self.config.identityClientSideCertificate != nil) { self.isConnectedWithClientSideCertificate = YES; } XRPerformBlockSynchronouslyOnMainQueue(^{ [self.client ircConnectionDidSecureConnection:self withProtocolType:protocolType cipherSuite:cipherSuite]; }); } - (void)ircConnectionDidCloseReadStream { self.EOFReceived = YES; XRPerformBlockSynchronouslyOnMainQueue(^{ [self.client ircConnectionDidCloseReadStream:self]; }); } - (void)ircConnectionDidDisconnectWithError:(nullable NSError *)disconnectError { self.connectionInvalidatedVoluntarily = YES; [self invalidateProcess]; [self _ircConnectionDidDisconnectWithError:disconnectError]; } - (void)_ircConnectionDidDisconnectWithError:(nullable NSError *)disconnectError { XRPerformBlockSynchronouslyOnMainQueue(^{ [self closeInsecureCertificateTrustPanel]; [self.client ircConnection:self didDisconnectWithError:disconnectError]; }); } - (void)ircConnectionDidReceiveData:(NSData *)data { /* IRCClient performs call to main thread later in stack. */ NSString *dataString = [self convertFromCommonEncoding:data]; if (dataString == nil) { return; } [self.client ircConnection:self didReceiveData:dataString]; } - (void)ircConnectionRequestInsecureCertificateTrust:(RCMTrustResponse)trustBlock { XRPerformBlockSynchronouslyOnMainQueue(^{ [self openInsecureCertificateTrustPanel:trustBlock]; }); } - (void)ircConnectionWillSendData:(NSData *)data { XRPerformBlockSynchronouslyOnMainQueue(^{ NSString *dataString = [self convertFromCommonEncoding:data]; if (dataString == nil) { return; } [self.client ircConnection:self willSendData:dataString]; }); } - (void)ircConnectionDidSendData { self.isSending = NO; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCExtras.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSStringHelper.h" #import "TXMasterController.h" #import "TXMenuControllerPrivate.h" #import "TDCAlert.h" #import "TPCPathInfo.h" #import "TLOLocalization.h" #import "TLOpenLink.h" #import "TVCMainWindow.h" #import "IRCClientConfig.h" #import "IRCClientPrivate.h" #import "IRCChannelConfig.h" #import "IRCChannel.h" #import "IRCServer.h" #import "IRCWorldPrivate.h" #import "IRCExtrasPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCExtras + (void)performSpecialActionForTextualScheme:(NSString *)action source:(NSString *)sourceLocation { /* Syntax: textual:// Reserved tokens: acknowledgements — Open acknowledgements file activate-license — Activate a license key application-support-folder — Open the Application Support folder contributors — Open contributors file custom-scripts-folder – Open the custom scripts storage location folder custom-style-folder — Open the custom style storage location folder custom-styles-folder — Open the custom style storage location folder diagnostic-reports-folder — System diagnostic reports folder goto — Navigate to an item knowledge-base — Open the homepage of our knowledge base newsletter — Open the subscription page for the newsletter support-channel — Connect to the #textual channel testing-channel — Connect to the #textual-testing channel unsupervised-script-folder — Open the custom scripts storage location folder unsupervised-scripts-folder — Open the custom scripts storage location folder */ if ([action isEqualToString:@"acknowledgements"]) { [menuController() openAcknowledgements:nil]; } #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 else if ([action isEqualToString:@"activate-license"]) { NSURL *licenseKeyURL = [NSURL URLWithString:sourceLocation]; [menuController() manageLicense:nil activateLicenseKeyWithURL:licenseKeyURL]; } #endif else if ([action isEqualToString:@"application-support-folder"]) { [RZWorkspace() openURL:[TPCPathInfo groupContainerApplicationSupportURL]]; } else if ([action isEqualToString:@"contributors"]) { [menuController() openAcknowledgements:nil]; } else if ([action isEqualToString:@"custom-scripts-folder"] || [action isEqualToString:@"unsupervised-script-folder"] || [action isEqualToString:@"unsupervised-scripts-folder"]) { [RZWorkspace() openURL:[TPCPathInfo customScriptsURL]]; } else if ([action isEqualToString:@"custom-style-folder"] || [action isEqualToString:@"custom-styles-folder"]) { [RZWorkspace() openURL:[TPCPathInfo customThemesURL]]; } else if ([action isEqualToString:@"diagnostic-reports-folder"]) { [RZWorkspace() openURL:[TPCPathInfo userDiagnosticReportsURL]]; [RZWorkspace() openURL:[TPCPathInfo systemDiagnosticReportsURL]]; } else if ([action isEqualToString:@"goto"]) { NSURL *url = [NSURL URLWithString:sourceLocation]; [menuController() navigateToTreeItemAtURL:url]; } else if ([action isEqualToString:@"knowledge-base"]) { [TLOpenLink openWithString:@"https://help.codeux.com/textual/" inBackground:NO]; } else if ([action isEqualToString:@"newsletter"]) { [TLOpenLink openWithString:@"https://www.codeux.com/textual/newsletter/" inBackground:NO]; } else if ([action isEqualToString:@"support-channel"]) { [menuController() connectToTextualHelpChannel:nil]; } else if ([action isEqualToString:@"testing-channel"]) { [menuController() connectToTextualTestingChannel:nil]; } } + (void)parseIRCProtocolURI:(NSString *)location { [self parseIRCProtocolURI:location withDescriptor:nil]; } + (void)parseIRCProtocolURI:(NSString *)location withDescriptor:(nullable NSAppleEventDescriptor *)event { NSParameterAssert(location != nil); /* Basic input clean up. */ NSString *locationValue = location; locationValue = locationValue.percentDecodedString; locationValue = locationValue.trim; /* This method extracts the path component of the URL from the input string before turning the remaining pieces into an NSURL. */ /* The URL may contain multiple sections proceeded by the pound sign (#), which this method treats as channel, but NSURL aren't too friendly about. */ NSUInteger totalSlashCount = [locationValue occurrencesOfCharacter:'/']; if (totalSlashCount < 2 || totalSlashCount > 3) { return; } NSString *serverInfo = locationValue; NSString *channelInfo = nil; if (totalSlashCount == 3) { // Only cut if we do have an extra slash. NSRange backwardRange = [locationValue rangeOfString:@"/" options:NSBackwardsSearch]; if (backwardRange.location != NSNotFound) { serverInfo = [locationValue substringToIndex:backwardRange.location]; channelInfo = [locationValue substringAfterIndex:backwardRange.location]; } } /* Now that channel information is no longer present in the URL, we can pass it to NSURL to extract all other information. */ NSURL *baseURL = [NSURL URLWithString:serverInfo]; NSString *addressScheme = baseURL.scheme; NSString *serverAddress = baseURL.host; if (addressScheme == nil || serverAddress == nil) { return; } if ([addressScheme isEqualToString:@"textual"]) { [self performSpecialActionForTextualScheme:serverAddress source:locationValue]; return; } /* Continue normal parsing... */ NSNumber *serverPort = baseURL.port; if (serverPort == nil) { serverPort = @(IRCConnectionDefaultServerPort); } __block BOOL connectSecurely = NO; if ([addressScheme isEqualToString:@"ircs"]) { connectSecurely = YES; } /* If we have made it to this point without this method returning, then everything is going smooth so far. We have established our server address, the URL scheme, and associated channel information. */ /* We will now parse the actual channel information. */ /* This method does not actually create the connection. It only formats the input so that another can. Therefore, we do not have to take much care with the channel information. Just a basic parse to establish if the "needssl" token is present as well as the channel name having a pound (#) sign in front of it. */ NSMutableString *channelList = [NSMutableString string]; if (channelInfo) { NSArray *dataSections = [channelInfo split:@","]; NSUInteger dataSectionsCount = dataSections.count; [dataSections enumerateObjectsUsingBlock:^(NSString *section, NSUInteger index, BOOL *stop) { if (section.length == 0) { return; } if (index > 4) { *stop = YES; return; } BOOL isLastObject = ((index + 1) == dataSectionsCount); if (isLastObject && [section isEqualToStringIgnoringCase:@"needssl"]) { connectSecurely = YES; return; } NSString *sectionCopy = section; if ([sectionCopy hasPrefix:@"#"] == NO) { sectionCopy = [@"#" stringByAppendingString:sectionCopy]; } [channelList appendString:sectionCopy]; [channelList appendString:@","]; }]; /* Erase end commas */ if (channelList.length > 1) { [channelList deleteCharactersInRange:NSMakeRange((channelList.length - 1), 1)]; } } /* We have parsed every part of our URL. Build the final result and pass it along. We are done here. */ NSString *resultValue = nil; if (connectSecurely) { resultValue = [NSString stringWithFormat:@"-SSL %@:%hu", serverAddress, serverPort.unsignedShortValue]; } else { resultValue = [NSString stringWithFormat:@"%@:%hu", serverAddress, serverPort.unsignedShortValue]; } /* A URL is consider untrusted and will not auto connect */ [self createConnectionToServer:resultValue channelList:channelList connectWhenCreated:NO mergeConnectionIfPossible:YES selectFirstChannelAdded:NO]; } + (void)createConnectionToServer:(NSString *)serverInfo channelList:(nullable NSString *)channelList connectWhenCreated:(BOOL)connectWhenCreated { [self createConnectionToServer:serverInfo channelList:channelList connectWhenCreated:connectWhenCreated mergeConnectionIfPossible:NO selectFirstChannelAdded:NO]; } + (void)createConnectionToServer:(NSString *)serverInfo channelList:(nullable NSString *)channelList connectWhenCreated:(BOOL)connectWhenCreated mergeConnectionIfPossible:(BOOL)mergeConnectionIfPossible selectFirstChannelAdded:(BOOL)selectFirstChannelAdded { NSParameterAssert(serverInfo != nil); /* Establish our variables */ NSString *serverAddress = nil; uint16_t serverPort = IRCConnectionDefaultServerPort; NSString *serverPassword = nil; BOOL connectSecurely = NO; /* Begin parsing */ NSMutableString *serverInfoMutable = [serverInfo mutableCopy]; /* Get our first token. A token is everything before the first occurrence of a space character. getToken will get everything before a space in a string, then erase the remaining content of that string so that each call to getToken gives us the next section of our string. */ NSString *tempStore = serverInfoMutable.token; /* Secure Socket Layer? */ if ([tempStore isEqualToStringIgnoringCase:@"-SSL"] || [tempStore isEqualToStringIgnoringCase:@"-TLS"]) { connectSecurely = YES; /* If the SSL define was our first token, we go to our next token. */ tempStore = serverInfoMutable.token; } /* Server Address */ NSInteger openingBracketPosition = ([tempStore stringPosition:@"["] + 1); NSInteger closingBracketPosition = [tempStore stringPosition:@"]"]; BOOL hasOpeningBracket = (openingBracketPosition == 1 && openingBracketPosition < closingBracketPosition); BOOL hasClosingBracket = (closingBracketPosition > 0 && openingBracketPosition < closingBracketPosition); if (hasOpeningBracket && hasClosingBracket) { NSRange serverAddressRange = NSMakeRange(openingBracketPosition, (closingBracketPosition - openingBracketPosition)); NSString *tempServerAddress = [tempStore substringWithRange:serverAddressRange]; if (tempServerAddress.IPv6Address == NO) { LogToConsoleError("Server address was surrounded by square brackets but the enclosed value was not an IPv6 address"); return; } serverAddress = tempServerAddress; tempStore = [tempStore substringAfterIndex:closingBracketPosition]; } else if (hasOpeningBracket == NO && hasClosingBracket == NO) { /* Our server address did not contain brackets. Does it contain a colon (:) which means a port is included? */ NSInteger colonPosition = [tempStore stringPosition:@":"]; if (colonPosition > (-1)) { serverAddress = [tempStore substringToIndex:colonPosition]; /* We cut the server address out of our temporary store, but left the colon and everything after it, in it. */ tempStore = [tempStore substringFromIndex:colonPosition]; } else { serverAddress = tempStore; } } else { /* If we have a opening bracket but no closing or any combination of the two, then return this method as our server address is already invalid. If there were not brackets either, then we are not treating the server as an IPv4 address so any colon will be considered for port use only. */ return; } if (serverAddress.validInternetAddress == NO) { LogToConsoleError("Invalid internet address"); return; } serverAddress = serverAddress.lowercaseString; /* Server Port */ NSString *tempServerPort = nil; if ([tempStore hasPrefix:@":"]) { tempServerPort = [tempStore substringFromIndex:1]; } else if (serverInfoMutable.length > 0) { tempServerPort = serverInfoMutable.token; } if (tempServerPort) { if ([tempServerPort hasPrefix:@"+"]) { tempServerPort = [tempServerPort substringFromIndex:1]; connectSecurely = YES; } if (tempServerPort.validInternetPort == NO) { LogToConsoleError("Invalid internet port"); return; } serverPort = tempServerPort.integerValue; } /* Server Password */ /* If our base is still not empty after taking out the token for the server address and port, then we are going to treat that as the server password. Anything after this token will be ignored completely. */ if (serverInfoMutable.length > 0) { tempStore = serverInfoMutable.token; serverPassword = tempStore; } /* Convert channel list string into array of configurations */ NSMutableArray *channelListArray = nil; if (channelList.length > 0) { channelListArray = [NSMutableArray array]; NSArray *dataSections = [channelList split:@","]; for (NSString *section in dataSections) { NSString *channelName = section.trim; if (channelName.isChannelName == NO) { continue; } if ([channelListArray containsObjectIgnoringCase:channelName]) { continue; } [channelListArray addObject:channelName]; } } /* Create connection */ [self createConnectionToServer:serverAddress serverPort:serverPort serverPassword:serverPassword connectSecurely:connectSecurely channelList:[channelListArray copy] connectWhenCreated:connectWhenCreated mergeConnectionIfPossible:mergeConnectionIfPossible selectFirstChannelAdded:selectFirstChannelAdded]; } + (void)createConnectionToServer:(NSString *)serverAddress serverPort:(uint16_t)serverPort serverPassword:(nullable NSString *)serverPassword connectSecurely:(BOOL)connectSecurely channelList:(nullable NSArray *)channelList connectWhenCreated:(BOOL)connectWhenCreated mergeConnectionIfPossible:(BOOL)mergeConnectionIfPossible selectFirstChannelAdded:(BOOL)selectFirstChannelAdded { NSParameterAssert(serverAddress != nil); NSParameterAssert(serverPort > 0); NSUInteger channelListCount = channelList.count; /* If merging is enabled, try to find first possible client by comparing server address values. */ /* Merging is only performed if a channel is being joined. */ IRCClient *existingClient = nil; if (mergeConnectionIfPossible && channelListCount > 0) { existingClient = [worldController() findClientWithServerAddress:serverAddress]; } if (existingClient != nil) { BOOL mergeConnection = NO; if (channelListCount > 1) { NSString *channelListFormatted = [channelList componentsJoinedByString:@", "]; mergeConnection = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[a9z-9f]", existingClient.name) title:TXTLS(@"Prompts[pnc-ew]", serverAddress, channelListFormatted) defaultButton:TXTLS(@"Prompts[0hh-sl]") alternateButton:TXTLS(@"Prompts[sv9-8s]")]; } else { mergeConnection = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[mx1-qz]", existingClient.name) title:TXTLS(@"Prompts[3l6-3z]", serverAddress, channelList.firstObject) defaultButton:TXTLS(@"Prompts[sl5-rf]") alternateButton:TXTLS(@"Prompts[xca-5h]")]; } // YES = default button (create new connection) if (mergeConnection == NO) { existingClient = nil; } } /* Create new connection or merge into existing */ if (existingClient) { IRCChannel *firstChannelAdded = nil; for (NSString *channelName in channelList) { IRCChannel *channel = [existingClient findChannelOrCreate:channelName isPrivateMessage:NO]; if (firstChannelAdded == nil) { firstChannelAdded = channel; } if (connectWhenCreated) { [existingClient joinChannel:channel]; } } [worldController() save]; if (selectFirstChannelAdded && firstChannelAdded) { [mainWindow() select:firstChannelAdded]; } } else // existingClient { IRCClientConfigMutable *baseConfig = [IRCClientConfigMutable new]; baseConfig.connectionName = serverAddress; IRCServerMutable *server = [IRCServerMutable new]; server.serverAddress = serverAddress; server.serverPort = serverPort; server.prefersSecuredConnection = connectSecurely; if (serverPassword != nil) { server.serverPassword = serverPassword; } baseConfig.serverList = @[[server copy]]; NSMutableArray *channelListConfigs = [NSMutableArray arrayWithCapacity:channelListCount]; for (NSString *channelName in channelList) { IRCChannelConfig *channelConfig = [IRCChannelConfig seedWithName:channelName]; [channelListConfigs addObject:channelConfig]; } baseConfig.channelList = channelListConfigs; IRCClient *client = [worldController() createClientWithConfig:baseConfig reload:YES]; [worldController() save]; if (connectWhenCreated) { [client connect]; } if (selectFirstChannelAdded) { [client selectFirstChannelInChannelList]; } } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCHighlightLogEntry.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSColorHelper.h" #import "NSStringHelper.h" #import "NSTableViewHelperPrivate.h" #import "TXGlobalModels.h" #import "TXMasterController.h" #import "IRCChannel.h" #import "IRCWorld.h" #import "TLOLocalization.h" #import "TLONotificationControllerPrivate.h" #import "TVCLogLinePrivate.h" #import "IRCHighlightLogEntryInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCHighlightLogEntry - (NSString *)description { IRCChannel *channel = self.channel; if (channel == nil) { return [super description]; } TVCLogLine *logLine = self.lineLogged; return [logLine renderedBodyForTranscriptLogInChannel:channel]; } - (NSString *)timeLoggedFormatted { TVCLogLine *logLine = self.lineLogged; NSTimeInterval timeInterval = logLine.receivedAt.timeIntervalSinceNow; NSString *formattedTimeInterval = TXHumanReadableTimeInterval(timeInterval, YES, 0); return TXTLS(@"BasicLanguage[4um-w4]", formattedTimeInterval); } - (nullable IRCChannel *)channel { IRCChannel *channel = [worldController() findChannelWithId:self.channelId onClientWithId:self.clientId]; return channel; } - (NSString *)channelName { IRCChannel *channel = self.channel; if (channel) { return channel.name; } return TXTLS(@"BasicLanguage[vbl-xi]"); } - (NSAttributedString *)renderedMessage { IRCChannel *channel = self.channel; TVCLogLine *logLine = self.lineLogged; NSString *nicknameBody = nil; NSString *messageBody = nil; if (logLine.lineType == TVCLogLineTypeAction) { /* Actions are presented in the format "• : _lineLogged != nil); NSParameterAssert(self->_clientId.length > 0); NSParameterAssert(self->_channelId.length > 0); } - (void)populateDuringCopy:(__kindof XRPortablePropertyObject *)newObject mutableCopy:(BOOL)mutableCopy { IRCHighlightLogEntry *object = (IRCHighlightLogEntry *)newObject; object->_lineLogged = self->_lineLogged; object->_clientId = self->_clientId; object->_channelId = self->_channelId; } - (__kindof XRPortablePropertyObject *)mutableClass { return [IRCHighlightLogEntryMutable self]; } @end #pragma mark - @implementation IRCHighlightLogEntryMutable @dynamic lineLogged; @dynamic clientId; @dynamic channelId; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [IRCHighlightLogEntry self]; } - (void)setLineLogged:(TVCLogLine *)lineLogged { NSParameterAssert(lineLogged != nil); if (self->_lineLogged != lineLogged) { self->_lineLogged = [lineLogged copy]; } } - (void)setClientId:(NSString *)clientId { NSParameterAssert(clientId != nil); if (self->_clientId != clientId) { self->_clientId = [clientId copy]; } } - (void)setChannelId:(NSString *)channelId { NSParameterAssert(channelId != nil); if (self->_channelId != channelId) { self->_channelId = [channelId copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCHighlightMatchCondition.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCHighlightMatchConditionInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCHighlightMatchCondition - (instancetype)init { return [super initWithDictionary:@{}]; } - (void)initializedClassHealthCheck { if (self.mutable || self.initializedAsCopy) { return; } NSParameterAssert(self->_matchKeyword.length > 0); } - (void)populateDictionaryValues:(NSDictionary *)dic { NSParameterAssert(dic != nil); [dic assignBoolTo:&self->_matchIsExcluded forKey:@"matchIsExcluded"]; [dic assignStringTo:&self->_matchChannelId forKey:@"matchChannelID"]; [dic assignStringTo:&self->_matchKeyword forKey:@"matchKeyword"]; [dic assignStringTo:&self->_uniqueIdentifier forKey:@"uniqueIdentifier"]; } - (void)populateDefaultsPostflight { SetVariableIfNil(self->_matchKeyword, @"") SetVariableIfNil(self->_uniqueIdentifier, [NSString stringWithUUID]) } - (NSDictionary *)dictionaryValueForTarget:(XRPortablePropertyDictTarget)target { NSMutableDictionary *dic = [NSMutableDictionary dictionary]; [dic maybeSetObject:self.matchChannelId forKey:@"matchChannelID"]; [dic maybeSetObject:self.matchKeyword forKey:@"matchKeyword"]; [dic maybeSetObject:self.uniqueIdentifier forKey:@"uniqueIdentifier"]; [dic setBool:self.matchIsExcluded forKey:@"matchIsExcluded"]; return [dic copy]; } - (id)uniqueCopyAsMutable:(BOOL)mutableCopy { IRCHighlightMatchCondition *object = [super uniqueCopyAsMutable:mutableCopy]; object->_uniqueIdentifier = [NSString stringWithUUID]; return object; } - (__kindof XRPortablePropertyDict *)mutableClass { return [IRCHighlightMatchConditionMutable self]; } @end #pragma mark - @implementation IRCHighlightMatchConditionMutable @dynamic matchChannelId; @dynamic matchIsExcluded; @dynamic matchKeyword; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyDict *)immutableClass { return [IRCHighlightMatchCondition self]; } - (void)setMatchIsExcluded:(BOOL)matchIsExcluded { if (self->_matchIsExcluded != matchIsExcluded) { self->_matchIsExcluded = matchIsExcluded; } } - (void)setMatchChannelId:(nullable NSString *)matchChannelId { if (self->_matchChannelId != matchChannelId) { self->_matchChannelId = [matchChannelId copy]; } } - (void)setMatchKeyword:(NSString *)matchKeyword { NSParameterAssert(matchKeyword != nil); if (self->_matchKeyword != matchKeyword) { self->_matchKeyword = [matchKeyword copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCISupportInfo.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual and/or Codeux Software, nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "TLOLocalization.h" #import "IRC.h" #import "IRCClientPrivate.h" #import "IRCModeInfo.h" #import "IRCISupportInfoPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _channelUserModeValue 100 @interface IRCISupportInfo () @property (nonatomic, weak) IRCClient *client; @property (nonatomic, copy) NSArray *cachedConfiguration; @property (nonatomic, assign, readwrite) NSUInteger maximumAwayLength; @property (nonatomic, assign, readwrite) NSUInteger maximumChannelNameLength; @property (nonatomic, assign, readwrite) NSUInteger maximumKeyLength; @property (nonatomic, assign, readwrite) NSUInteger maximumKickLength; @property (nonatomic, assign, readwrite) NSUInteger maximumNicknameLength; @property (nonatomic, assign, readwrite) NSUInteger maximumTopicLength; @property (nonatomic, assign, readwrite) NSUInteger maximumModeCount; @property (nonatomic, copy, readwrite) NSArray *channelNamePrefixes; @property (nonatomic, copy, readwrite) NSArray *statusMessageModeSymbols; @property (nonatomic, copy, readwrite) NSDictionary *channelModes; @property (nonatomic, copy, readwrite) NSDictionary *userModeSymbols; @property (nonatomic, copy, readwrite, nullable) NSString *banExceptionModeSymbol; @property (nonatomic, copy, readwrite, nullable) NSString *inviteExceptionModeSymbol; @property (nonatomic, copy, readwrite, nullable) NSString *networkName; @property (nonatomic, copy, readwrite, nullable) NSString *networkNameFormatted; @end @implementation IRCISupportInfo - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithClient:(IRCClient *)client { NSParameterAssert(client != nil); if ((self = [super init])) { self.client = client; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [self reset]; } - (void)reset { self.cachedConfiguration = @[]; self.serverAddress = nil; self.networkName = nil; self.networkNameFormatted = nil; self.channelNamePrefixes = @[@"#"]; self.maximumModeCount = TXMaximumNodesPerModeCommand; self.maximumNicknameLength = IRCProtocolDefaultNicknameMaximumLength; self.userModeSymbols = @{ @"modeSymbols" : @[@"o", @"v"], @"characters" : @[@"@", @"+"] }; self.channelModes = @{ @"o" : @(_channelUserModeValue), @"v" : @(_channelUserModeValue) }; self.statusMessageModeSymbols = @[]; } - (void)processConfigurationData:(NSString *)configurationData { NSParameterAssert(configurationData != nil); configurationData = configurationData.trim; if (configurationData.length == 0) { return; } IRCClient *client = self.client; NSMutableDictionary *configuration = [NSMutableDictionary dictionary]; NSArray *segments = [configurationData componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *segment in segments) { if (segment.length == 0) { // Blank continue; } NSString *segmentKey = segment; NSString *segmentValue = nil; NSInteger equalSignPosition = [segment stringPosition:@"="]; if (equalSignPosition > 0) { segmentKey = [segment substringToIndex:equalSignPosition]; segmentValue = [segment substringAfterIndex:equalSignPosition]; if (segmentValue.length == 0) { segmentValue = nil; } } if (segmentValue) { configuration[segmentKey] = segmentValue; } else { configuration[segmentKey] = @(YES); } if (segmentValue) { if ([segmentKey isEqualToStringIgnoringCase:@"AWAYLEN"]) { NSInteger awayLength = segmentValue.integerValue; if (awayLength > 0) { self.maximumAwayLength = awayLength; } } else if ([segmentKey isEqualToStringIgnoringCase:@"CHANMODES"]) { [self parseChannelModes:segmentValue]; } else if ([segmentKey isEqualToStringIgnoringCase:@"CHANNELLEN"]) { NSInteger channelNameLength = segmentValue.integerValue; if (channelNameLength > 0) { self.maximumChannelNameLength = channelNameLength; } } else if ([segmentKey isEqualToStringIgnoringCase:@"CHANTYPES"]) { NSArray *channelNamePrefixes = segmentValue.characterStringBuffer; if (channelNamePrefixes.count > 0) { self.channelNamePrefixes = channelNamePrefixes; } } else if ([segmentKey isEqualToStringIgnoringCase:@"KEYLEN"]) { NSInteger maximumKeyLength = segmentValue.integerValue; if (maximumKeyLength > 0) { self.maximumKeyLength = maximumKeyLength; } } else if ([segmentKey isEqualToStringIgnoringCase:@"KICKLEN"]) { NSInteger maximumKickLength = segmentValue.integerValue; if (maximumKickLength > 0) { self.maximumKickLength = maximumKickLength; } } else if ([segmentKey isEqualToStringIgnoringCase:@"MODES"]) { NSInteger maximumModesCount = segmentValue.integerValue; if (maximumModesCount > 0) { self.maximumModeCount = maximumModesCount; } } else if ([segmentKey isEqualToStringIgnoringCase:@"NETWORK"]) { self.networkName = segmentValue; self.networkNameFormatted = TXTLS(@"IRC[8hg-7k]", segmentValue); } else if ([segmentKey isEqualToStringIgnoringCase:@"NICKLEN"]) { NSInteger maximumNicknameLength = segmentValue.integerValue; if (maximumNicknameLength > 0) { self.maximumNicknameLength = maximumNicknameLength; } } else if ([segmentKey isEqualToStringIgnoringCase:@"PREFIX"]) { [self parseUserModeSymbols:segmentValue]; } else if ([segmentKey isEqualToStringIgnoringCase:@"STATUSMSG"]) { self.statusMessageModeSymbols = segmentValue.characterStringBuffer; } else if ([segmentKey isEqualToStringIgnoringCase:@"TOPICLEN"]) { NSInteger maximumTopicLength = segmentValue.integerValue; if (maximumTopicLength > 0) { self.maximumTopicLength = maximumTopicLength; } } } if ([segmentKey isEqualToStringIgnoringCase:@"EXCEPTS"]) { if (segmentValue.isModeSymbol) { self.banExceptionModeSymbol = segmentValue; } else { self.banExceptionModeSymbol = @"e"; } } else if ([segmentKey isEqualToStringIgnoringCase:@"INVEX"]) { if (segmentValue.isModeSymbol) { self.inviteExceptionModeSymbol = segmentValue; } else { self.inviteExceptionModeSymbol = @"I"; } } else if ([segmentKey isEqualToStringIgnoringCase:@"MONITOR"]) { // freenode advertises support for MONITOR but does not respond to command /* Update as of May 30, 2018: [17:25:55] Back in 2016 I hard coded MONITOR support disabled on freenode in my client because back then the network advertised it but didn't actually respond to it. Has that since changed? [17:26:53] <@Unit193> milky: Depends on the server unfortunately. [17:27:24] Unit193 so just keep it disabled, you think? [17:27:39] <@Unit193> milky: Yeah, the server I'm on claims support. */ if ([self.serverAddress hasSuffix:@".freenode.net"]) { continue; } [client enableCapability:ClientIRCv3SupportedCapabilityMonitorCommand]; } else if ([segmentKey isEqualToStringIgnoringCase:@"NAMESX"]) { if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityMultiPrefix] == NO) { [client sendLine:@"PROTOCTL NAMESX"]; [client enableCapability:ClientIRCv3SupportedCapabilityMultiPrefix]; } } else if ([segmentKey isEqualToStringIgnoringCase:@"UHNAMES"]) { if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityUserhostInNames] == NO) { [client sendLine:@"PROTOCTL UHNAMES"]; [client enableCapability:ClientIRCv3SupportedCapabilityUserhostInNames]; } } else if ([segmentKey isEqualToStringIgnoringCase:@"WATCH"]) { [client enableCapability:ClientIRCv3SupportedCapabilityWatchCommand]; } } // while() self.cachedConfiguration = [self.cachedConfiguration arrayByAddingObject:configuration]; } - (nullable NSString *)stringValueForConfiguration:(NSDictionary *)configuration { NSParameterAssert(configuration != nil); /* This takes our cached configuration data and builds it into what it would look like if we were to receive an actual 005. The only difference is this method formats each token that is in our configuration cache to make them easier to see. We use bold for the tokens. This is pretty much only used in developer mode, but it could have other uses? */ if (configuration.count == 0) { return nil; } NSMutableString *stringValue = [NSMutableString string]; NSArray *sortedKeys = configuration.sortedDictionaryKeys; for (NSString *key in sortedKeys) { id value = configuration[key]; if ([value isKindOfClass:[NSString class]]) { [stringValue appendFormat:@"\002%@\002=%@ ", key, value]; } else { [stringValue appendFormat:@"\002%@ \002", key]; } } return [stringValue copy]; } - (nullable NSString *)stringValueForLastUpdate { NSDictionary *configuration = self.cachedConfiguration.lastObject; if (configuration == nil) { return nil; } return [self stringValueForConfiguration:configuration]; } - (NSArray *)parseModes:(NSString *)modeString { NSParameterAssert(modeString != nil); NSMutableArray *modes = [NSMutableArray array]; NSMutableString *modeStringMutable = [modeString mutableCopy]; BOOL modeIsSet = NO; do { NSString *nextToken = modeStringMutable.token; if (nextToken.length == 0) { break; } UniChar nextCharacter = [nextToken characterAtIndex:0]; if (nextCharacter != '+' && nextCharacter != '-') { continue; } modeIsSet = (nextCharacter == '+'); nextToken = [nextToken substringFromIndex:1]; for (NSUInteger i = 0; i < nextToken.length; i++) { nextCharacter = [nextToken characterAtIndex:i]; if (nextCharacter == '-') { modeIsSet = NO; } else if (nextCharacter == '+') { modeIsSet = YES; } else { NSString *modeSymbol = [NSString stringWithUniChar:nextCharacter]; IRCModeInfoMutable *mode = [[IRCModeInfoMutable alloc] initWithModeSymbol:modeSymbol modeIsSet:modeIsSet]; if ([self modeHasParameter:modeSymbol whenModeIsSet:modeIsSet]) { mode.modeParameter = modeStringMutable.token; } [modes addObject:[mode copy]]; } } } while (modeStringMutable.length > 0); return [modes copy]; } - (void)parseUserModeSymbols:(NSString *)modeString { NSParameterAssert(modeString != nil); // Format: (qaohv)~&@%+ /* Perform validation on placement of parentheses */ NSInteger openingParenthesesPosition = [modeString stringPosition:@"("]; NSInteger closingParenthesesPosition = [modeString stringPosition:@")"]; if (openingParenthesesPosition != 0 && openingParenthesesPosition >= closingParenthesesPosition) { return; } /* Extract relevant information and ensure that they are equal length */ NSString *modeSymbols = [modeString substringWithRange:NSMakeRange(1, (closingParenthesesPosition - 1))]; NSString *characters = [modeString substringAfterIndex:closingParenthesesPosition]; if (modeSymbols.length != characters.length) { return; } /* Begin processing modes */ /* The mode symbols and characters are stored in separate arrays because NSDictionary has no sense of order and the order of the user mode symbols is very important to establish rank. */ NSArray *modeSymbolsArray = modeSymbols.characterStringBuffer; NSArray *charactersArray = characters.characterStringBuffer; self.userModeSymbols = @{ @"modeSymbols" : modeSymbolsArray, @"characters" : charactersArray }; /* Update channel modes array so that we know these mode symbols are for user */ NSMutableDictionary *channelModes = [self.channelModes mutableCopy]; for (NSString *modeSymbol in modeSymbolsArray) { channelModes[modeSymbol] = @(_channelUserModeValue); } self.channelModes = channelModes; } - (BOOL)modeHasParameter:(NSString *)modeSymbol whenModeIsSet:(BOOL)whenModeIsSet { NSParameterAssert(modeSymbol != nil); // Input: CHANMODES=A,B,C,D // // A = Always has a parameter. Index: 1 // B = Always has a parameter. Index: 2 // C = Only has a parameter when set. Index: 3 // D = Never has a parameter. Index: 4 NSUInteger modeIndex = [self.channelModes unsignedIntegerForKey:modeSymbol]; if (modeIndex == 1 || modeIndex == 2 || modeIndex == _channelUserModeValue) { return YES; } else if (modeIndex == 3) { return whenModeIsSet; } return NO; } - (void)parseChannelModes:(NSString *)modeString { NSParameterAssert(modeString != nil); // Input: CHANMODES=A,B,C,D // // A = Always has a parameter. Index: 1 // B = Always has a parameter. Index: 2 // C = Only has a parameter when set. Index: 3 // D = Never has a parameter. Index: 4 NSMutableDictionary *channelModes = [self.channelModes mutableCopy]; NSArray *modes = [modeString split:@","]; for (NSUInteger i = 0; i < modes.count; i++) { NSString *modeSet = modes[i]; for (NSUInteger j = 0; j < modeSet.length; j++) { NSString *modeSymbol = [modeSet stringCharacterAtIndex:j]; channelModes[modeSymbol] = @(i + 1); } } self.channelModes = channelModes; } - (nullable NSString *)userPrefixForModeSymbol:(NSString *)modeSymbol { NSParameterAssert(modeSymbol != nil); NSArray *modeSymbols = self.userModeSymbols[@"modeSymbols"]; NSUInteger modeSymbolIndex = [modeSymbols indexOfObject:modeSymbol]; if (modeSymbolIndex == NSNotFound) { return 0; } NSArray *characters = self.userModeSymbols[@"characters"]; return characters[modeSymbolIndex]; } - (BOOL)modeSymbolIsUserPrefix:(NSString *)modeSymbol { NSParameterAssert(modeSymbol != nil); return ([self userPrefixForModeSymbol:modeSymbol] != nil); } - (nullable NSString *)modeSymbolForUserPrefix:(NSString *)character { NSParameterAssert(character != nil); NSArray *characters = self.userModeSymbols[@"characters"]; NSUInteger characterIndex = [characters indexOfObject:character]; if (characterIndex == NSNotFound) { return nil; } NSArray *modeSymbols = self.userModeSymbols[@"modeSymbols"]; return modeSymbols[characterIndex]; } - (BOOL)characterIsUserPrefix:(NSString *)character { NSParameterAssert(character != nil); return ([self modeSymbolForUserPrefix:character] != nil); } - (NSUInteger)rankForUserPrefixWithMode:(NSString *)modeSymbol { NSParameterAssert(modeSymbol != nil); NSArray *modeSymbols = self.userModeSymbols[@"modeSymbols"]; NSUInteger modeSymbolIndex = [modeSymbols indexOfObject:modeSymbol]; if (modeSymbolIndex == NSNotFound) { return 0; } return (IRCISupportInfoHighestUserPrefixRank - modeSymbolIndex); } - (NSString *)extractStatusMessagePrefixFromChannelNamed:(NSString *)channel { NSArray *characters = self.statusMessageModeSymbols; return [self _extractCharacters:characters fromChannelNamed:channel]; } - (NSString *)_extractCharacters:(NSArray *)characters fromChannelNamed:(NSString *)channel { NSParameterAssert(characters != nil); NSParameterAssert(channel != nil); if (channel.length < 2) { return @""; } NSArray *channelNamePrefixes = self.channelNamePrefixes; for (NSString *character in characters) { if ([channel hasPrefix:character] == NO) { continue; } NSString *nextCharacter = [channel stringCharacterAtIndex:1]; if ([channelNamePrefixes containsObject:nextCharacter]) { return character; } } return @""; } - (IRCModeInfo *)createModeWithSymbol:(NSString *)modeSymbol { NSParameterAssert(modeSymbol != nil); return [[IRCModeInfo alloc] initWithModeSymbol:modeSymbol]; } - (IRCModeInfo *)createModeWithSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameter:(nullable NSString *)modeParameter { NSParameterAssert(modeSymbol != nil); return [[IRCModeInfo alloc] initWithModeSymbol:modeSymbol modeIsSet:modeIsSet modeParameter:modeParameter]; } - (BOOL)configurationReceived { return (self.cachedConfiguration.count > 0); } - (BOOL)isListSupported:(IRCISupportInfoListType)listType { return ([self modeSymbolForList:listType] != nil); } - (nullable NSString *)modeSymbolForList:(IRCISupportInfoListType)listType { switch (listType) { case IRCISupportInfoListTypeBan: { return @"b"; } case IRCISupportInfoListTypeBanException: { return self.banExceptionModeSymbol; } case IRCISupportInfoListTypeInviteException: { return self.inviteExceptionModeSymbol; } case IRCISupportInfoListTypeQuiet: { /* +q is used by some servers as the user mode for channel owner. If this mode is a user mode, then hide the menu item. */ if ([self modeSymbolIsUserPrefix:@"q"]) { return nil; } return @"q"; } default: { return nil; } } // switch } - (nullable NSString *)statusMessagePrefixForModeSymbol:(NSString *)modeSymbol { NSParameterAssert(modeSymbol != nil); NSString *character = [self userPrefixForModeSymbol:modeSymbol]; if (character == nil) { return nil; } if ([self.statusMessageModeSymbols containsObject:character] == NO) { return nil; } return character; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCMessage.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "TXGlobalModelsPrivate.h" #import "IRCClientPrivate.h" #import "IRCPrefix.h" #import "IRCMessageInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCMessage DESIGNATED_INITIALIZER_EXCEPTION_BODY_BEGIN - (instancetype)init { if ((self = [super init])) { [self populateDefaultsPostflight]; return self; } return nil; } DESIGNATED_INITIALIZER_EXCEPTION_BODY_END - (nullable instancetype)initWithLine:(NSString *)line { return [self initWithLine:line onClient:nil]; } - (nullable instancetype)initWithLine:(NSString *)line onClient:(nullable IRCClient *)client { if ((self = [super init])) { BOOL parseResult = [self parseLine:line forClient:client]; if (parseResult == NO) { return nil; } [self populateDefaultsPostflight]; return self; } return nil; } - (void)populateDefaultsPostflight { SetVariableIfNil(self->_command, @"") SetVariableIfNil(self->_messageTags, @{}) SetVariableIfNil(self->_params, @[]) SetVariableIfNil(self->_receivedAt, [NSDate date]) SetVariableIfNil(self->_sender, [IRCPrefix new]) } - (NSUInteger)paramsCount { return self.params.count; } - (NSString *)paramAt:(NSUInteger)index { if (index < self.params.count) { return self.params[index]; } return @""; } - (NSString *)sequence { if (self.params.count < 2) { return [self sequence:0]; } else { return [self sequence:1]; } } - (NSString *)sequence:(NSUInteger)index { NSMutableString *sequence = [NSMutableString string]; NSArray *params = self.params; NSUInteger paramsCount = params.count; for (NSUInteger i = index; i < paramsCount; i++) { NSString *param = params[i]; if (i != index) { [sequence appendString:@" "]; } [sequence appendString:param]; } return [sequence copy]; } - (void)markAsNotHistoric { self->_isHistoric = NO; } - (nullable NSString *)senderNickname { return self.sender.nickname; } - (nullable NSString *)senderUsername { return self.sender.username; } - (nullable NSString *)senderAddress { return self.sender.address; } - (nullable NSString *)senderHostmask { return self.sender.hostmask; } - (BOOL)senderIsServer { return self.sender.isServer; } - (void)populateDuringCopy:(__kindof XRPortablePropertyObject *)newObject mutableCopy:(BOOL)mutableCopy { IRCMessage *object = (IRCMessage *)newObject; object->_batchToken = self->_batchToken; object->_command = self->_command; object->_commandNumeric = self->_commandNumeric; object->_isHistoric = self->_isHistoric; object->_isEventOnlyMessage = self->_isEventOnlyMessage; object->_isPrintOnlyMessage = self->_isPrintOnlyMessage; object->_messageTags = self->_messageTags; object->_params = self->_params; object->_receivedAt = self->_receivedAt; object->_sender = self->_sender; } - (__kindof XRPortablePropertyObject *)mutableClass { return [IRCMessageMutable self]; } @end #pragma mark - @implementation IRCMessage (IRCMessageLineParser) - (BOOL)parseLine:(NSString *)line forClient:(nullable IRCClient *)client { NSParameterAssert(line != nil); NSMutableString *lineMutable = [line mutableCopy]; /* Parse extension information (if present) */ if ([lineMutable hasPrefix:@"@"]) { NSString *extensionInfo = lineMutable.token; if (extensionInfo.length <= 1) { return NO; } extensionInfo = [extensionInfo substringFromIndex:1]; [self parseExtensions:extensionInfo forClient:client]; } /* Parse sender information (if present) */ if ([lineMutable hasPrefix:@":"]) { NSString *senderInfo = lineMutable.token; if (senderInfo.length <= 1) { return NO; } senderInfo = [senderInfo substringFromIndex:1]; [self parseSender:senderInfo forClient:client]; } else { /* If the line does not have a sender, then we use the server address as the sender. If that isn't known, then we use the the address the user has configured. */ /* -serverAddress is only nil when the client isn't connected anywhere. We are parsing messages when connected somewhere so it's safe to cast it as as non-nil at least here. */ NSString * _Nonnull serverAddress = (NSString * _Nonnull)client.serverAddress; IRCPrefixMutable *sender = [IRCPrefixMutable new]; sender.nickname = serverAddress; sender.hostmask = serverAddress; sender.isServer = YES; self->_sender = [sender copy]; } /* Parse command */ NSString *command = lineMutable.token; if (command.length < 1) { return NO; } if (command.isNumericOnly) { self->_command = [command copy]; self->_commandNumeric = command.integerValue; } else { self->_command = [command.uppercaseString copy]; self->_commandNumeric = 0; } /* Parse remaining data */ NSMutableArray *parameters = [NSMutableArray new]; while (lineMutable.length > 0) { if ([lineMutable hasPrefix:@":"]) { NSString *sequence = [lineMutable substringFromIndex:1]; [parameters addObject:sequence]; break; } else { NSString *sequence = lineMutable.token; [parameters addObject:sequence]; } } self->_params = [parameters copy]; /* Return success */ return YES; } - (void)parseExtensions:(NSString *)extensionInfo forClient:(nullable IRCClient *)client { NSParameterAssert(extensionInfo != nil); /* Chop the tags up using ; as a divider as defined by the syntax located at: */ /* An example grouping would look like the following: @aaa=bbb;ccc;example.com/ddd=eee */ NSDictionary *extensions = [extensionInfo formDataUsingSeparator:@";" decodingBlock:^NSString *(NSString *value) { return value.decodedMessageTagString; }]; self->_messageTags = [extensions copy]; /* If there is no client, then further processing is not possible */ if (client == nil) { return; } /* Check for known capacities */ if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityServerTime]) { /* We support two time extensions. The time= value is the date and time in the format as defined by ISO 8601:2004(E) 4.3.2. */ /* The t= value is a legacy value in a epoch time. We always favor the new time= format over the old. */ NSString *dateString = extensions[@"time"]; if (dateString == nil) { dateString = extensions[@"t"]; } if (dateString) { NSDate *dateObject = nil; if ([dateString onlyContainsCharactersFromCharacterSet:[NSCharacterSet ZeroToNineDecimalCharacterSet]]) { dateObject = [NSDate dateWithTimeIntervalSince1970:dateString.doubleValue]; } else { dateObject = [TXSharedISOStandardDateFormatter() dateFromString:dateString]; } if (dateObject) { self->_receivedAt = [dateObject copy]; self->_isHistoric = YES; } } } if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityBatch]) { NSString *batchToken = extensions[@"batch"]; if ([batchToken onlyContainsCharactersFromCharacterSet:[NSCharacterSet Ato9UnderscoreDash]]) { self->_batchToken = [batchToken copy]; self->_parentBatchMessage = [client queuedBatchMessageWithToken:batchToken]; } } } - (void)parseSender:(NSString *)senderInfo forClient:(nullable IRCClient *)client { NSParameterAssert(senderInfo != nil); IRCPrefixMutable *sender = [IRCPrefixMutable new]; NSString *senderNickname = nil; NSString *senderUsername = nil; NSString *senderAddress = nil; sender.hostmask = senderInfo;// Declare entire section as host /* Parse the user info into their appropriate sections or return NO if we can't. */ if ([senderInfo hostmaskComponents:&senderNickname username:&senderUsername address:&senderAddress onClient:client]) { sender.nickname = senderNickname; sender.username = senderUsername; sender.address = senderAddress; } else { sender.nickname = senderInfo; sender.isServer = YES; } self->_sender = [sender copy]; } @end #pragma mark - @implementation IRCMessageMutable @dynamic batchToken; @dynamic command; @dynamic commandNumeric; @dynamic isHistoric; @dynamic isEventOnlyMessage; @dynamic isPrintOnlyMessage; @dynamic messageTags; @dynamic params; @dynamic receivedAt; @dynamic sender; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [IRCMessage self]; } - (void)setBatchToken:(nullable NSString *)batchToken { if (self->_batchToken != batchToken) { self->_batchToken = [batchToken copy]; } } - (void)setCommand:(NSString *)command { NSParameterAssert(command != nil); if (self->_command != command) { self->_command = [command copy]; } } - (void)setCommandNumeric:(NSUInteger)commandNumeric { if (self->_commandNumeric != commandNumeric) { self->_commandNumeric = commandNumeric; } } - (void)setIsHistoric:(BOOL)isHistoric { if (self->_isHistoric != isHistoric) { self->_isHistoric = isHistoric; } } - (void)setIsEventOnlyMessage:(BOOL)isEventOnlyMessage { if (self->_isEventOnlyMessage != isEventOnlyMessage) { self->_isEventOnlyMessage = isEventOnlyMessage; } } - (void)setIsPrintOnlyMessage:(BOOL)isPrintOnlyMessage { if (self->_isPrintOnlyMessage != isPrintOnlyMessage) { self->_isPrintOnlyMessage = isPrintOnlyMessage; } } - (void)setMessageTags:(nullable NSDictionary *)messageTags { if (self->_messageTags != messageTags) { self->_messageTags = [messageTags copy]; } } - (void)setParams:(NSArray *)params { NSParameterAssert(params != nil); if (self->_params != params) { self->_params = [params copy]; } } - (void)setReceivedAt:(NSDate *)receivedAt { NSParameterAssert(receivedAt != nil); if (self->_receivedAt != receivedAt) { self->_receivedAt = [receivedAt copy]; } } - (void)setSender:(IRCPrefix *)sender { NSParameterAssert(sender != nil); if (self->_sender != sender) { self->_sender = [sender copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCMessageBatch.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCMessage.h" #import "IRCMessageBatchPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCMessageBatchMessageContainer () @property (nonatomic, strong) NSMutableDictionary *internalBatchEntries; @end @interface IRCMessageBatchMessage () @property (nonatomic, strong) NSMutableArray *internalBatchEntries; @end #pragma mark - @implementation IRCMessageBatchMessageContainer - (NSDictionary *)queuedEntries { @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { return @{}; } return [self.internalBatchEntries copy]; } } - (void)dequeueEntries { @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { return; } [self.internalBatchEntries removeAllObjects]; } } - (void)dequeueEntry:(id)entry { NSParameterAssert(entry != nil); @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { return; } NSString *batchToken = nil; if ([entry isKindOfClass:[IRCMessageBatchMessage class]]) { batchToken = [entry batchToken]; } else if ([entry isKindOfClass:[NSString class]]) { batchToken = entry; entry = self.internalBatchEntries[batchToken]; } if (batchToken == nil) { return; } [entry dequeueEntries]; [self.internalBatchEntries removeObjectForKey:batchToken]; } } - (void)queueEntry:(id)entry { NSParameterAssert(entry != nil); if ([entry isKindOfClass:[IRCMessageBatchMessage class]] == NO) { return; } NSString *batchToken = [entry batchToken]; @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { self.internalBatchEntries = [NSMutableDictionary dictionary]; } self.internalBatchEntries[batchToken] = entry; } } - (id)queuedEntryWithBatchToken:(NSString *)batchToken { NSParameterAssert(batchToken != nil); @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { return nil; } return self.internalBatchEntries[batchToken]; } } @end #pragma mark - @implementation IRCMessageBatchMessage - (NSArray *)queuedEntries { @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { return @[]; } return [self.internalBatchEntries copy]; } } - (void)dequeueEntries { @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { return; } [self.internalBatchEntries removeAllObjects]; } } - (void)queueEntry:(id)entry { NSParameterAssert(entry != nil); if ([entry isKindOfClass:[IRCMessage class]] == NO && [entry isKindOfClass:[IRCMessageBatchMessage class]] == NO) { return; } @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { self.internalBatchEntries = [NSMutableArray array]; } [self.internalBatchEntries addObject:entry]; } } - (void)dequeueEntry:(id)entry { NSParameterAssert(entry != nil); if ([entry isKindOfClass:[IRCMessage class]] == NO && [entry isKindOfClass:[IRCMessageBatchMessage class]] == NO) { return; } @synchronized(self.internalBatchEntries) { if (self.internalBatchEntries == nil) { return; } [self.internalBatchEntries removeObject:entry]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCModeInfo.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCClient.h" #import "IRCISupportInfo.h" #import "IRCModeInfoInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCModeInfo - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithModeSymbol:(NSString *)modeSymbol { return [self initWithModeSymbol:modeSymbol modeIsSet:NO modeParameter:nil]; } - (instancetype)initWithModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet { return [self initWithModeSymbol:modeSymbol modeIsSet:modeIsSet modeParameter:nil]; } - (instancetype)initWithModeSymbol:(NSString *)modeSymbol modeIsSet:(BOOL)modeIsSet modeParameter:(nullable NSString *)modeParameter { NSParameterAssert(modeSymbol.length == 1); if ((self = [super init])) { self->_modeSymbol = [modeSymbol copy]; self->_modeIsSet = modeIsSet; self->_modeParameter = [modeParameter copy]; return self; } return nil; } - (void)populateDuringCopy:(__kindof XRPortablePropertyObject *)newObject mutableCopy:(BOOL)mutableCopy { IRCModeInfo *object = (IRCModeInfo *)newObject; object->_modeSymbol = self->_modeSymbol; object->_modeIsSet = self->_modeIsSet; object->_modeParameter = self->_modeParameter; } - (__kindof XRPortablePropertyObject *)mutableClass { return [IRCModeInfoMutable self]; } - (BOOL)isEqual:(id)object { if (object == nil) { return NO; } if (object == self) { return YES; } if ([object isKindOfClass:[IRCModeInfo class]] == NO) { return NO; } IRCModeInfo *objectCast = (IRCModeInfo *)object; return (self.modeIsSet == objectCast.modeIsSet && ((self.modeSymbol == nil && objectCast.modeSymbol == nil) || [self.modeSymbol isEqualToString:objectCast.modeSymbol]) && ((self.modeParameter == nil && objectCast.modeParameter == nil) || [self.modeParameter isEqualToString:objectCast.modeParameter])); } - (BOOL)isModeForChangingMemberModeOn:(IRCClient *)client { NSParameterAssert(client != nil); if (self.modeParameter.length == 0) { return NO; } return [client.supportInfo modeSymbolIsUserPrefix:self.modeSymbol]; } @end #pragma mark - @implementation IRCModeInfoMutable @dynamic modeIsSet; @dynamic modeSymbol; @dynamic modeParameter; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [IRCModeInfo self]; } - (void)setModeIsSet:(BOOL)modeIsSet { if (self->_modeIsSet != modeIsSet) { self->_modeIsSet = modeIsSet; } } - (void)setModeSymbol:(NSString *)modeSymbol { NSParameterAssert(modeSymbol.length == 1); if (self->_modeSymbol != modeSymbol) { self->_modeSymbol = [modeSymbol copy]; } } - (void)setModeParameter:(nullable NSString *)modeParameter { if (self->_modeParameter != modeParameter) { self->_modeParameter = [modeParameter copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCNetworkList.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TPCResourceManager.h" #import "IRCNetworkList.h" NS_ASSUME_NONNULL_BEGIN @interface IRCNetworkList () @property (nonatomic, copy, readwrite) NSArray *listOfNetworks; @end @interface IRCNetwork () @property (nonatomic, copy, readwrite) NSString *networkName; @property (nonatomic, copy, readwrite) NSString *serverAddress; @property (nonatomic, assign, readwrite) uint16_t serverPort; @property (nonatomic, assign, readwrite) BOOL prefersSecuredConnection; - (instancetype)initWithNetworkNamed:(NSString *)networkName networkConfiguration:(NSDictionary *)networkConfiguration; @end @implementation IRCNetworkList - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { NSMutableArray *listOfNetworks = [NSMutableArray array]; NSDictionary *networkList = [TPCResourceManager dictionaryFromResources:@"IRCNetworks" cacheValue:NO]; NSArray *networkNamesUnsorted = networkList.allKeys; NSArray *networkNamesSorted = [networkNamesUnsorted sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; for (NSString *networkName in networkNamesSorted) { NSDictionary *networkConfiguration = networkList[networkName]; IRCNetwork *networkObject = [[IRCNetwork alloc] initWithNetworkNamed:networkName networkConfiguration:networkConfiguration]; [listOfNetworks addObject:networkObject]; } self.listOfNetworks = listOfNetworks; } - (nullable IRCNetwork *)networkNamed:(NSString *)networkName { NSParameterAssert(networkName != nil); IRCNetwork *network = [self.listOfNetworks objectPassingTest:^BOOL(IRCNetwork *network, NSUInteger index, BOOL *stop) { return [network.networkName isEqualToStringIgnoringCase:networkName]; }]; return network; } - (nullable IRCNetwork *)networkWithServerAddress:(NSString *)serverAddress { NSParameterAssert(serverAddress != nil); IRCNetwork *network = [self.listOfNetworks objectPassingTest:^BOOL(IRCNetwork *network, NSUInteger index, BOOL *stop) { return [network.serverAddress isEqualToStringIgnoringCase:serverAddress]; }]; return network; } @end #pragma mark - @implementation IRCNetwork - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithNetworkNamed:(NSString *)networkName networkConfiguration:(NSDictionary *)networkConfiguration { NSParameterAssert(networkName != nil); NSParameterAssert(networkConfiguration != nil); if ((self = [super init])) { self.networkName = networkName; [self populateNetworkConfiguration:networkConfiguration]; return self; } return nil; } - (void)populateNetworkConfiguration:(NSDictionary *)networkConfiguration { NSParameterAssert(networkConfiguration != nil); self.serverAddress = networkConfiguration[@"serverAddress"]; self.serverPort = [networkConfiguration unsignedShortForKey:@"serverPort"]; self.prefersSecuredConnection = [networkConfiguration boolForKey:@"prefersSecuredConnection"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCPrefix.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCPrefixInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCPrefix - (void)populateDefaultsPostflight { SetVariableIfNil(self->_nickname, @"") SetVariableIfNil(self->_hostmask, @"") } - (void)populateDuringCopy:(__kindof XRPortablePropertyObject *)newObject mutableCopy:(BOOL)mutableCopy { IRCPrefix *object = (IRCPrefix *)newObject; object->_isServer = self->_isServer; object->_hostmask = self->_hostmask; object->_nickname = self->_nickname; object->_username = self->_username; object->_address = self->_address; } - (__kindof XRPortablePropertyObject *)mutableClass { return [IRCPrefixMutable self]; } - (BOOL)isEqual:(id)object { if (object == nil) { return NO; } if (object == self) { return YES; } if ([object isKindOfClass:[IRCPrefix class]] == NO) { return NO; } IRCPrefix *objectCast = (IRCPrefix *)object; return (self.isServer == objectCast.isServer && ((self.nickname == nil && objectCast.nickname == nil) || [self.nickname isEqualToString:objectCast.nickname]) && ((self.username == nil && objectCast.username == nil) || [self.username isEqualToString:objectCast.username]) && ((self.address == nil && objectCast.address == nil) || [self.address isEqualToString:objectCast.address]) && ((self.hostmask == nil && objectCast.hostmask == nil) || [self.hostmask isEqualToString:objectCast.hostmask])); } @end #pragma mark - @implementation IRCPrefixMutable @dynamic isServer; @dynamic nickname; @dynamic username; @dynamic address; @dynamic hostmask; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [IRCPrefix self]; } - (void)setIsServer:(BOOL)isServer { if (self->_isServer != isServer) { self->_isServer = isServer; } } - (void)setNickname:(NSString *)nickname { NSParameterAssert(nickname != nil); if (self->_nickname != nickname) { self->_nickname = [nickname copy]; } } - (void)setUsername:(nullable NSString *)username { if (self->_username != username) { self->_username = [username copy]; } } - (void)setAddress:(nullable NSString *)address { if (self->_address != address) { self->_address = [address copy]; } } - (void)setHostmask:(NSString *)hostmask { NSParameterAssert(hostmask != nil); if (self->_hostmask != hostmask) { self->_hostmask = [hostmask copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCSendingMessage.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCCommandIndex.h" #import "IRCSendingMessage.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCSendingMessage + (NSString *)stringWithCommand:(NSString *)command arguments:(nullable NSArray *)arguments { NSParameterAssert(command != nil); NSString *commandUppercase = command.uppercaseString; if (arguments.count == 0) { return commandUppercase; } NSMutableString *builtString = [NSMutableString stringWithString:commandUppercase]; NSInteger colonIndexBase = [IRCCommandIndex colonPositionForRemoteCommand:command]; NSInteger colonIndexCount = 0; for (NSString *argument in arguments) { if (argument.length == 0) { break; } [builtString appendString:@" "]; if (colonIndexBase == NSNotFound) { // Guess where the colon (:) should go. // // A colon is supposed to represent a section of an outgoing command // that has a parameter which contains spaces. For example, PRIVMSG // is in the format "PRIVMSG #channel :long message" — The message // will have spaces part of it, so we inform the server. if (colonIndexCount == (arguments.count - 1) && ([argument hasPrefix:@":"] || [argument contains:@" "])) { [builtString appendString:@":"]; } } else { // We know where it goes thanks to the command index if (colonIndexCount == colonIndexBase) { [builtString appendString:@":"]; } } [builtString appendString:argument]; colonIndexCount += 1; } return [builtString copy]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCServer.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRC.h" #import "IRCConnectionConfig.h" #import "IRCServerInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCServer - (instancetype)init { return [super initWithDictionary:@{}]; } - (void)initializedClassHealthCheck { if (self.initializedAsCopy) { return; } /* Health checks are disabled because Server Properties might write an empty server address to the class then perform a copy on the object which would throw an exception. */ /* TODO: Modify Server Properties to be more friendly. */ #if 0 if (self.mutable) { return; } NSParameterAssert(self->_serverAddress.length > 0); NSParameterAssert(self->_serverPort > 0 && self->_serverPort <= TXMaximumTCPPort); #endif } - (void)populateDefaultsPreflight { if (self.initializedAsCopy) { return; } self->_defaults = @{ @"serverPort" : @(IRCConnectionDefaultServerPort) }; } - (void)populateDefaultsPostflight { if (self.initializedAsCopy) { return; } SetVariableIfNil(self->_serverAddress, @"") SetVariableIfNil(self->_uniqueIdentifier, [NSString stringWithUUID]) } - (void)populateDictionaryValues:(NSDictionary *)dic { NSParameterAssert(dic != nil); NSMutableDictionary *defaultsMutable = [self->_defaults mutableCopy]; [defaultsMutable addEntriesFromDictionary:dic]; [defaultsMutable assignBoolTo:&self->_prefersSecuredConnection forKey:@"prefersSecuredConnection"]; [defaultsMutable assignStringTo:&self->_serverAddress forKey:@"serverAddress"]; [defaultsMutable assignStringTo:&self->_uniqueIdentifier forKey:@"uniqueIdentifier"]; [defaultsMutable assignUnsignedShortTo:&self->_serverPort forKey:@"serverPort"]; } - (NSDictionary *)dictionaryValueForTarget:(XRPortablePropertyDictTarget)target { NSMutableDictionary *dic = [NSMutableDictionary dictionary]; [dic setBool:self.prefersSecuredConnection forKey:@"prefersSecuredConnection"]; [dic maybeSetObject:self.serverAddress forKey:@"serverAddress"]; [dic maybeSetObject:self.uniqueIdentifier forKey:@"uniqueIdentifier"]; [dic setUnsignedShort:self.serverPort forKey:@"serverPort"]; return [dic copy]; } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { IRCServer *config = [self allocForCopyAsMutable:mutableCopy]; config->_defaults = self->_defaults; config->_serverPassword = self->_serverPassword; config->_destroyKeychainItemsDuringDealloc = self->_destroyKeychainItemsDuringDealloc; if (uniquing) { config->_uniqueIdentifier = [NSString stringWithUUID]; } return [config initWithDictionary:self.dictionaryValueForCopy]; } - (__kindof XRPortablePropertyDict *)mutableClass { return [IRCServerMutable self]; } - (void)dealloc { if (self.destroyKeychainItemsDuringDealloc) { [self destroyServerPasswordKeychainItem]; } } #pragma mark - #pragma mark Keychain Management - (nullable NSString *)serverPassword { if (self->_serverPassword) { return self->_serverPassword; } return self.serverPasswordFromKeychain; } - (nullable NSString *)serverPasswordFromKeychain { NSString *serverPasswordServiceName = [NSString stringWithFormat:@"textual.server.%@", self.uniqueIdentifier]; NSString *kcPassword = [XRKeychain getPasswordFromKeychainItem:@"Textual (Server Password)" withItemKind:@"application password" forUsername:nil serviceName:serverPasswordServiceName]; return kcPassword; } - (void)writeServerPasswordToKeychain { if (self->_serverPassword == nil) { return; } NSString *serverPasswordServiceName = [NSString stringWithFormat:@"textual.server.%@", self.uniqueIdentifier]; [XRKeychain modifyOrAddKeychainItem:@"Textual (Server Password)" withItemKind:@"application password" forUsername:nil withNewPassword:self->_serverPassword serviceName:serverPasswordServiceName]; self->_serverPassword = nil; } - (void)destroyServerPasswordKeychainItem { NSString *serverPasswordServiceName = [NSString stringWithFormat:@"textual.server.%@", self.uniqueIdentifier]; [XRKeychain deleteKeychainItem:@"Textual (Server Password)" withItemKind:@"application password" forUsername:nil serviceName:serverPasswordServiceName]; self->_serverPassword = nil; } @end #pragma mark - @implementation IRCServerMutable @dynamic prefersSecuredConnection; @dynamic serverAddress; @dynamic serverPassword; @dynamic serverPort; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyDict *)immutableClass { return [IRCServer self]; } - (void)setPrefersSecuredConnection:(BOOL)prefersSecuredConnection { if (self->_prefersSecuredConnection != prefersSecuredConnection) { self->_prefersSecuredConnection = prefersSecuredConnection; } } - (void)setServerAddress:(NSString *)serverAddress { NSParameterAssert(serverAddress != nil); if (self->_serverAddress != serverAddress) { self->_serverAddress = [serverAddress copy]; } } - (void)setServerPassword:(nullable NSString *)serverPassword { if (self->_serverPassword != serverPassword) { self->_serverPassword = [serverPassword copy]; } } - (void)setServerPort:(uint16_t)serverPort { if (self->_serverPort != serverPort) { self->_serverPort = serverPort; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCTimerCommand.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClientPrivate.h" #import "IRCChannel.h" #import "NSObjectHelperPrivate.h" #import "TLOTimer.h" #import "IRCTimerCommandPrivate.h" NS_ASSUME_NONNULL_BEGIN static NSUInteger IRCTimedCommandLastIdentifier = 0; @interface IRCTimedCommand () @property (nonatomic, copy, readwrite) NSString *identifier; @property (nonatomic, copy, readwrite) NSString *clientId; @property (nonatomic, copy, nullable, readwrite) NSString *channelId; @property (nonatomic, copy, readwrite) NSString *command; @property (nonatomic, strong) TLOTimer *timer; @property (nonatomic, assign) BOOL startedBefore; @end @implementation IRCTimedCommand - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithCommand:(NSString *)command onClient:(IRCClient *)client { NSParameterAssert(command != nil); NSParameterAssert(client != nil); return [self initWithCommand:command onClient:client inChannel:nil]; } - (instancetype)initWithCommand:(NSString *)command onClient:(IRCClient *)client inChannel:(nullable IRCChannel *)channel { NSParameterAssert(command != nil); NSParameterAssert(client != nil); if ((self = [super init])) { self.clientId = client.uniqueIdentifier; self.channelId = channel.uniqueIdentifier; self.command = command; [self assignIdentifier]; [self initTimerForClient:client]; return self; } return nil; } - (void)dealloc { [self.timer stop]; self.timer = nil; } - (void)assignIdentifier { IRCTimedCommandLastIdentifier++; self.identifier = [NSString stringWithUnsignedInteger:IRCTimedCommandLastIdentifier]; } - (void)initTimerForClient:(IRCClient *)client { NSParameterAssert(client != nil); __weak typeof(self) weakSelf = self; self.timer = [TLOTimer timerWithActionBlock:^(TLOTimer * _Nonnull sender) { [client onTimedCommand:weakSelf]; }]; } - (void)start:(NSTimeInterval)timerInterval { [self start:timerInterval onRepeat:NO iterations:0]; } - (void)start:(NSTimeInterval)timerInterval onRepeat:(BOOL)repeatTimer { [self start:timerInterval onRepeat:repeatTimer iterations:0]; } - (void)start:(NSTimeInterval)timerInterval onRepeat:(BOOL)repeatTimer iterations:(NSUInteger)iterations { [self.timer start:timerInterval onRepeat:repeatTimer iterations:iterations]; self.startedBefore = YES; } - (void)stop { [self.timer stop]; } - (BOOL)restart { if (self.startedBefore == NO) { return NO; } [self start:self.timerInterval onRepeat:self.repeatTimer iterations:self.iterations]; return YES; } - (NSTimeInterval)timerInterval { return self.timer.interval; } - (BOOL)timerIsActive { return self.timer.timerIsActive; } - (BOOL)repeatTimer { return self.timer.repeatTimer; } - (NSUInteger)iterations { return self.timer.iterations; } - (NSUInteger)currentIteration { return self.timer.currentIteration; } - (NSTimeInterval)startTime { return self.timer.startTime; } - (NSTimeInterval)timeRemaining { return self.timer.timeRemaining; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCTreeItem.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCTreeItemPrivate.h" NS_ASSUME_NONNULL_BEGIN #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wincomplete-implementation" @implementation IRCTreeItem - (BOOL)isUnread { return (self.treeUnreadCount > 0); } - (void)resetState { self.dockUnreadCount = 0; self.nicknameHighlightCount = 0; self.treeUnreadCount = 0; } @end #pragma clang diagnostic pop NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/IRCWorld.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual and/or Codeux Software, nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXMasterController.h" #import "TXMenuControllerPrivate.h" #import "TVCLogControllerPrivate.h" #import "TVCMainWindowPrivate.h" #import "TVCServerListPrivate.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TPCThemeController.h" #import "TPCTheme.h" #import "IRCClientConfig.h" #import "IRCClientPrivate.h" #import "IRCChannelPrivate.h" #import "IRCServer.h" #import "IRCTreeItemPrivate.h" #import "IRCWorldPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _autoConnectDelay 1 #define _reconnectAfterWakeupDelay 8 #define _savePeriodicallyThreshold 300 NSString * const IRCWorldClientListDefaultsKey = @"World Controller Client Configurations"; NSString * const IRCWorldClientListWasModifiedNotification = @"IRCWorldClientListWasModifiedNotification"; NSString * const IRCWorldDateHasChangedNotification = @"IRCWorldDateHasChangedNotification"; NSString * const IRCWorldWillDestroyClientNotification = @"IRCWorldWillDestroyClientNotification"; NSString * const IRCWorldWillDestroyChannelNotification = @"IRCWorldWillDestroyChannelNotification"; @interface IRCWorld () @property (nonatomic, strong) NSMutableArray *clients; @property (nonatomic, assign) BOOL preferencesDidChangeTimerIsActive; @property (nonatomic, assign) CFAbsoluteTime savePeriodicallyLastSave; @property (nonatomic, copy) NSDate *lastDateHasChangedDate; @end @implementation IRCWorld #pragma mark - #pragma mark Initialization - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.clients = [NSMutableArray new]; self.preferencesDidChangeTimerIsActive = NO; self.savePeriodicallyLastSave = CFAbsoluteTimeGetCurrent(); } - (void)dealloc { [self cancelPerformRequests]; } #pragma mark - #pragma mark Configuration - (void)setupConfiguration { self.isImportingConfiguration = YES; [mainWindowServerList() beginUpdates]; NSArray *clientList = [TPCPreferences clientList]; for (NSDictionary *client in clientList) { IRCClientConfig *e = [[IRCClientConfig alloc] initWithDictionary:client]; [self createClientWithConfig:e reload:YES]; } [mainWindowServerList() endUpdates]; self.isImportingConfiguration = NO; [self setupOtherServices]; } - (void)setupOtherServices { [self setupMidnightTimer]; [RZNotificationCenter() addObserver:self selector:@selector(dateChanged:) name:NSSystemClockDidChangeNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(userDefaultsDidChange:) name:TPCPreferencesUserDefaultsDidChangeNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(mainWindowAppearanceChanged:) name:TVCMainWindowAppearanceChangedNotification object:nil]; } - (NSArray *)clientConfigurations { NSMutableArray *configurations = [NSMutableArray array]; for (IRCClient *u in self.clientList) { [configurations addObject:[u configurationDictionary]]; } return [configurations copy]; } - (void)save { NSArray *clientList = [self clientConfigurations]; [TPCPreferences setClientList:clientList]; } - (void)savePeriodically { CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); if ((self.savePeriodicallyLastSave + _savePeriodicallyThreshold) < now) { self.savePeriodicallyLastSave = now; [self save]; } } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Preparing IRC world"); LogToConsoleTerminationProgress("Removing IRC world observers"); [RZNotificationCenter() removeObserver:self]; LogToConsoleTerminationProgress("Preparing clients: %{public}ld", self.clientCount); for (IRCClient *u in self.clientList) { [u prepareForApplicationTermination]; } } - (void)userDefaultsDidChange:(NSNotification *)notification { if (themeSettings().js_postPreferencesDidChangesNotifications == NO) { return; // Cancel operation... } if (self.preferencesDidChangeTimerIsActive == NO) { self.preferencesDidChangeTimerIsActive = YES; [self performSelectorInCommonModes:@selector(informAllViewsUserDefaultsDidChange) withObject:nil afterDelay:1.0]; } } - (void)informAllViewsUserDefaultsDidChange { self.preferencesDidChangeTimerIsActive = NO; [self evaluateFunctionOnAllViews:@"Textual.preferencesDidChange" arguments:nil onQueue:YES]; } - (void)mainWindowAppearanceChanged:(NSNotification *)notification { if (themeSettings().js_postAppearanceChangesNotification == NO) { return; } [self informAllViewsMainWindowAppearanceChanged]; } - (void)informAllViewsMainWindowAppearanceChanged { TVCMainWindowAppearance *appearance = mainWindow().userInterfaceObjects; [self evaluateFunctionOnAllViews:@"Textual.appearanceDidChange" arguments:@[appearance.shortAppearanceDescription] onQueue:YES]; } #pragma mark - #pragma mark Properties - (NSArray *)clientList { @synchronized(self.clients) { return [self.clients copy]; } } - (void)setClientList:(NSArray *)clientList { @synchronized(self.clients) { [self.clients removeAllObjects]; [self.clients addObjectsFromArray:clientList]; [self postClientListWasModifiedNotification]; } } - (NSUInteger)clientCount { @synchronized(self.clients) { return self.clients.count; } } #pragma mark - #pragma mark Utilities - (void)postClientListWasModifiedNotification { [RZNotificationCenter() postNotificationName:IRCWorldClientListWasModifiedNotification object:self]; } - (void)autoConnectAfterWakeup:(BOOL)afterWakeUp { if (masterController().ghostModeIsOn && afterWakeUp == NO) { return; } NSUInteger delay = 0; if (afterWakeUp) { delay += _reconnectAfterWakeupDelay; } #define _isAutoConnecting (afterWakeUp == NO && u.config.autoConnect) #define _isWakingFromSleep (afterWakeUp && u.config.autoSleepModeDisconnect && u.disconnectType == IRCClientDisconnectModeComputerSleep) for (IRCClient *u in self.clientList) { if (_isWakingFromSleep == NO && _isAutoConnecting == NO) { continue; } [u autoConnectWithDelay:delay afterWakeUp:afterWakeUp]; delay += _autoConnectDelay; } #undef _isWakingFromSleep #undef _isAutoConnecting } - (void)prepareForSleep { if ([TPCPreferences disconnectOnSleep] == NO) { return; } for (IRCClient *u in self.clientList) { if (u.isConnected == NO) { continue; } u.disconnectType = IRCClientDisconnectModeComputerSleep; [u quit]; } } - (void)prepareForScreenSleep { if ([TPCPreferences setAwayOnScreenSleep] == NO) { return; } for (IRCClient *u in self.clientList) { [u toggleAwayStatus:YES]; } } - (void)wakeFromScreenSleep { if ([TPCPreferences setAwayOnScreenSleep] == NO) { return; } for (IRCClient *u in self.clientList) { [u toggleAwayStatus:NO]; } } - (void)noteReachabilityChanged:(BOOL)reachable { for (IRCClient *u in self.clientList) { [u noteReachabilityChanged:reachable]; } } - (void)preferencesChanged { [menuController() preferencesChanged]; for (IRCClient *u in self.clientList) { [u preferencesChanged]; } } - (void)setupMidnightTimer { [self setupMidnightTimerWithNotification:NO]; } - (void)setupMidnightTimerWithNotification:(BOOL)fireNotification { /* Ask for the day, month, and year from the current calendar. */ /* We are not asking for time which means that it will default to zero. */ NSDateComponents *currentDayComponents = [RZCurrentCalendar() components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay) fromDate:[NSDate date]]; NSDate *lastMidnight = [RZCurrentCalendar() dateFromComponents:currentDayComponents]; /* Create date components one day in the future. */ /* All other values default to zero. */ NSDateComponents *futureDayComponents = [NSDateComponents new]; futureDayComponents.day = 1; /* With the current date and future components, calculate the date on which our midnight timer will land. */ NSDate *nextMidnight = [RZCurrentCalendar() dateByAddingComponents:futureDayComponents toDate:lastMidnight options:0]; /* Create timer for midnight in future. */ /* We set the tolerance for the timer to absolute zero so that we are confident that OS X will not reschedule it. */ NSTimer *midnightTimer = [[NSTimer alloc] initWithFireDate:nextMidnight interval:0.0 target:self selector:@selector(dateChanged:) userInfo:nil repeats:NO]; midnightTimer.tolerance = 0.0; /* Schedule the timer on the run loop which will retain reference. */ [RZMainRunLoop() addTimer:midnightTimer forMode:NSDefaultRunLoopMode]; /* Do not fire notification if day hasn't changed. This check is down here, instead of at top, because we still want to reschedule timer when the system date changes. */ if (self.lastDateHasChangedDate != nil) { if ([self.lastDateHasChangedDate isInSameDayAsDate:lastMidnight]) { LogToConsoleFault("Date changed event received but the day hasn't changed"); return; } } self.lastDateHasChangedDate = lastMidnight; /* Post notification if needed. */ if (fireNotification) { [RZNotificationCenter() postNotificationName:IRCWorldDateHasChangedNotification object:nil userInfo:nil]; [self evaluateFunctionOnAllViews:@"Textual.dateChanged" arguments:@[@(currentDayComponents.year), @(currentDayComponents.month), @(currentDayComponents.day)] onQueue:NO]; } } - (void)dateChanged:(id)sender { /* We call the notifications in the timer so we do not have to ask for the current day components two times. */ [self setupMidnightTimerWithNotification:YES]; } #pragma mark - #pragma mark Tree Items - (NSArray<__kindof IRCTreeItem *> *)findItemsWithIds:(NSArray *)itemIds { NSParameterAssert(itemIds != nil); NSMutableArray<__kindof IRCTreeItem *> *items = [NSMutableArray array]; for (IRCClient *u in self.clientList) { if ([itemIds containsObject:u.uniqueIdentifier]) { [items addObject:u]; } for (IRCChannel *c in u.channelList) { if ([itemIds containsObject:c.uniqueIdentifier]) { [items addObject:c]; } } } return [items copy]; } - (nullable IRCTreeItem *)findItemWithId:(NSString *)itemId { if (itemId == nil) { return nil; } for (IRCClient *u in self.clientList) { if ([itemId isEqualToString:u.uniqueIdentifier]) { return u; } for (IRCChannel *c in u.channelList) { if ([itemId isEqualToString:c.uniqueIdentifier]) { return c; } } } return nil; } - (nullable IRCClient *)findClientWithId:(NSString *)clientId { return (IRCClient *)[self findItemWithId:clientId]; } - (nullable IRCChannel *)findChannelWithId:(NSString *)channelId onClientWithId:(NSString *)clientId { return (IRCChannel *)[self findItemWithId:channelId]; } - (nullable IRCTreeItem *)findItemWithPasteboardString:(NSString *)string { return [self findItemWithId:string]; } - (NSString *)pasteboardStringForItem:(IRCTreeItem *)item { NSParameterAssert(item != nil); return item.uniqueIdentifier; } - (nullable IRCClient *)findClientWithServerAddress:(NSString *)serverAddress { for (IRCClient *u in self.clientList) { for (IRCServer *s in u.config.serverList) { if ([s.serverAddress isEqualToStringIgnoringCase:serverAddress]) { return u; } } } return nil; } #pragma mark - #pragma mark JavaScript - (void)evaluateFunctionOnAllViews:(NSString *)function arguments:(nullable NSArray *)arguments { [self evaluateFunctionOnAllViews:function arguments:arguments onQueue:YES]; } - (void)evaluateFunctionOnAllViews:(NSString *)function arguments:(nullable NSArray *)arguments onQueue:(BOOL)onQueue { NSParameterAssert(function != nil); if (masterController().applicationIsTerminating) { return; } for (IRCClient *u in self.clientList) { [u.viewController evaluateFunction:function withArguments:arguments onQueue:onQueue]; for (IRCChannel *c in u.channelList) { [c.viewController evaluateFunction:function withArguments:arguments onQueue:onQueue]; } } } #pragma mark - #pragma mark Factory - (IRCClient *)createClientWithConfig:(IRCClientConfig *)config { return [self createClientWithConfig:config reload:YES]; } - (IRCClient *)createClientWithConfig:(IRCClientConfig *)config reload:(BOOL)reload { NSParameterAssert(config != nil); IRCClient *client = [[IRCClient alloc] initWithConfig:config]; client.viewController = [self createViewControllerWithClient:client channel:nil]; NSMutableArray *channelList = [NSMutableArray array]; for (IRCChannelConfig *channelConfig in client.config.channelList) { IRCChannel *channel = [self createChannelWithConfig:channelConfig onClient:client add:NO adjust:NO reload:NO]; [channelList addObject:channel]; } client.channelList = channelList; @synchronized(self.clients) { [self.clients addObject:client]; if (reload) { NSInteger index = [self.clients indexOfObject:client]; [mainWindowServerList() addItemToList:index inParent:nil]; } if (self.clients.count == 1) { [mainWindow() select:client]; } } [mainWindow() reloadLoadingScreen]; [menuController() populateNavigationChannelList]; [self postClientListWasModifiedNotification]; return client; } - (IRCChannel *)createChannelWithConfig:(IRCChannelConfig *)config onClient:(IRCClient *)client { return [self createChannelWithConfig:config onClient:client add:YES adjust:YES reload:YES]; } - (IRCChannel *)createChannelWithConfig:(IRCChannelConfig *)config onClient:(IRCClient *)client add:(BOOL)add adjust:(BOOL)adjust reload:(BOOL)reload { NSParameterAssert(config != nil); NSParameterAssert(client != nil); IRCChannel *channel = [[IRCChannel alloc] initWithConfig:config]; channel.associatedClient = client; channel.viewController = [self createViewControllerWithClient:client channel:channel]; if (add) { [client addChannel:channel]; } if (reload) { NSInteger index = [client.channelList indexOfObject:channel]; [mainWindowServerList() addItemToList:index inParent:client]; } if (adjust) { [mainWindow() adjustSelection]; [menuController() populateNavigationChannelList]; } return channel; } - (IRCChannel *)createPrivateMessage:(NSString *)nickname onClient:(IRCClient *)client { return [self createPrivateMessage:nickname onClient:client asType:IRCChannelTypePrivateMessage]; } - (IRCChannel *)createPrivateMessage:(NSString *)nickname onClient:(IRCClient *)client asType:(IRCChannelType)type { NSParameterAssert(nickname != nil); NSParameterAssert(client != nil); NSParameterAssert(type == IRCChannelTypePrivateMessage || type == IRCChannelTypeUtility); IRCChannelConfigMutable *config = [IRCChannelConfigMutable new]; config.channelName = nickname; config.type = type; IRCChannel *channel = [self createChannelWithConfig:config onClient:client add:YES adjust:YES reload:YES]; if (client.isLoggedIn && channel.isPrivateMessage) { [channel activate]; } return channel; } - (TVCLogController *)createViewControllerWithClient:(IRCClient *)client channel:(nullable IRCChannel *)channel { NSParameterAssert(client != nil); TVCLogController *viewController = nil; if (channel == nil) { viewController = [[TVCLogController alloc] initWithClient:client inWindow:mainWindow()]; } else { viewController = [[TVCLogController alloc] initWithChannel:channel inWindow:mainWindow()]; } return viewController; } - (void)selectOtherBeforeDestroy:(IRCTreeItem *)target { NSParameterAssert(target != nil); if (target.isClient) { [mainWindow() deselectGroup:target]; } else { [mainWindow() deselect:target]; } } - (void)destroyClient:(IRCClient *)client { NSParameterAssert(client != nil); /* It is not safe to destroy the client while connected. */ if (client.isConnecting || client.isConnected) { __weak IRCWorld *weakSelf = self; __weak IRCClient *weakClient = client; client.disconnectCallback = ^{ [weakSelf destroyClient:weakClient]; }; [client quit]; return; } [RZNotificationCenter() postNotificationName:IRCWorldWillDestroyClientNotification object:client]; [self selectOtherBeforeDestroy:client]; [client prepareForPermanentDestruction]; @try { [mainWindowServerList() removeItemFromList:client]; } @catch (NSException *exception) { LogToConsoleError("Caught exception: %{public}@", exception.reason); LogStackTrace(); } @synchronized(self.clients) { [self.clients removeObjectIdenticalTo:client]; } [self postClientListWasModifiedNotification]; [mainWindow() reloadLoadingScreen]; [menuController() populateNavigationChannelList]; } - (void)destroyChannel:(IRCChannel *)channel { [self destroyChannel:channel reload:YES part:YES]; } - (void)destroyChannel:(IRCChannel *)channel reload:(BOOL)reload { [self destroyChannel:channel reload:reload part:YES]; } - (void)destroyChannel:(IRCChannel *)channel reload:(BOOL)reload part:(BOOL)partChannel { NSParameterAssert(channel != nil); [RZNotificationCenter() postNotificationName:IRCWorldWillDestroyChannelNotification object:channel]; IRCClient *client = channel.associatedClient; if (partChannel) { [client partChannel:channel]; } if (reload) { [self selectOtherBeforeDestroy:channel]; } [channel prepareForPermanentDestruction]; if (client.lastSelectedChannel == channel) { client.lastSelectedChannel = nil; } if (reload) { @try { [mainWindowServerList() removeItemFromList:channel]; } @catch (NSException *exception) { LogToConsoleError("Caught exception: %{public}@", exception.reason); LogStackTrace(); } [client removeChannel:channel]; [mainWindow() adjustSelection]; [menuController() populateNavigationChannelList]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/Users/IRCChannelUser.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TPCPreferencesLocal.h" #import "IRCClient.h" #import "IRCISupportInfo.h" #import "IRCUserPrivate.h" #import "IRCUserRelationsPrivate.h" #import "IRCChannelUserInternal.h" NS_ASSUME_NONNULL_BEGIN @interface IRCChannelUser () @property (atomic, strong, readwrite) IRCUser *user; @property (readonly) IRCClient *client; @property (readonly) NSString *highestRankedUserMode; @end @implementation IRCChannelUser - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithUser:(IRCUser *)user { NSParameterAssert(user != nil); if ((self = [super init])) { self.user = user; [self prepareInitialState]; [self populateDefaultsPostflight]; return self; } return nil; } - (void)prepareInitialState { self->_lastWeightFade = CFAbsoluteTimeGetCurrent(); self->_creationTime = [[NSDate date] timeIntervalSince1970]; } - (void)populateDefaultsPostflight { SetVariableIfNil(self->_modes, @"") } - (void)changeUserToUser:(IRCUser *)user { NSParameterAssert(user != nil); self.user = user; } - (IRCClient *)client { return self.user.client; } - (BOOL)userModesContainsMode:(NSString *)mode { return [self.modes contains:mode]; } - (NSString *)highestRankedUserMode { NSString *modes = self.modes; if (modes.length == 0) { return modes; } return [modes stringCharacterAtIndex:0]; } - (NSString *)mark { IRCISupportInfo *supportInfo = self.client.supportInfo; NSString *mode = self.highestRankedUserMode; NSString *mark = [supportInfo userPrefixForModeSymbol:mode]; if (mark) { return mark; } return @""; } - (NSUInteger)channelRank { IRCISupportInfo *supportInfo = self.client.supportInfo; NSString *mode = self.highestRankedUserMode; return [supportInfo rankForUserPrefixWithMode:mode]; } - (BOOL)isOp { return [self.modes containsCharacters:@"qOao"]; } - (BOOL)isHalfOp { return [self.modes containsCharacters:@"qOaoh"]; } - (IRCUserRank)rank { NSString *mode = self.highestRankedUserMode; return [self rankForModeSymbol:mode]; } - (IRCUserRank)ranks { IRCUserRank ranks = 0; NSString *modes = self.modes; for (NSUInteger i = 0; i < modes.length; i++) { NSString *mode = [modes stringCharacterAtIndex:i]; IRCUserRank rank = [self rankForModeSymbol:mode]; if (rank != IRCUserRankNone) { ranks |= rank; } } if (ranks == 0) { ranks |= IRCUserRankNone; } return ranks; } - (IRCUserRank)rankForModeSymbol:(nullable NSString *)modeSymbol { if (modeSymbol == nil) { return IRCUserRankNone; } if ([modeSymbol isEqualToString:@"y"] || [modeSymbol isEqualToString:@"Y"]) { return IRCUserRankIRCopByMode; } else if ([modeSymbol isEqualToString:@"q"] || [modeSymbol isEqualToString:@"O"]) { return IRCUserRankChannelOwner; } else if ([modeSymbol isEqualToString:@"a"]) { return IRCUserRankSuperOperator; } else if ([modeSymbol isEqualToString:@"o"]) { return IRCUserRankNormalOperator; } else if ([modeSymbol isEqualToString:@"h"]) { return IRCUserRankHalfOperator; } else if ([modeSymbol isEqualToString:@"v"]) { return IRCUserRankVoiced; } return IRCUserRankNone; } - (double)totalWeight { [self decayConversation]; return (self->_incomingWeight + self->_outgoingWeight); } - (void)outgoingConversation { double change = ((lrint(self->_outgoingWeight) == 0) ? 20 : 5); self->_outgoingWeight += change; } - (void)incomingConversation { double change = ((lrint(self->_incomingWeight) == 0) ? 100 : 20); self->_incomingWeight += change; } - (void)conversation { double change = ((lrint(self->_incomingWeight) == 0) ? 4 : 1); self->_incomingWeight += change; } - (void)decayConversation { CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); double minutes = ((now - self->_lastWeightFade) / 60); if (minutes > 1) { self->_lastWeightFade = now; if (self->_incomingWeight > 0) { self->_incomingWeight /= pow(2, minutes); } if (self->_outgoingWeight > 0) { self->_outgoingWeight /= pow(2, minutes); } } } - (NSComparisonResult)compareUsingWeights:(IRCChannelUser *)other { NSParameterAssert(other != nil); double localWeight = self.totalWeight; double remoteWeight = other.totalWeight; if (localWeight > remoteWeight) { return NSOrderedAscending; } else if (localWeight < remoteWeight) { return NSOrderedDescending; } return [self compareUsingRank:other]; } - (NSComparisonResult)compareUsingRank:(IRCChannelUser *)other { NSParameterAssert(other != nil); BOOL favorIRCop = [TPCPreferences memberListSortFavorsServerStaff]; if (favorIRCop && self.user.isIRCop && other.user.isIRCop == NO) { return NSOrderedAscending; } else if (favorIRCop && self.user.isIRCop == NO && other.user.isIRCop) { return NSOrderedDescending; } NSNumber *localRank = @([self channelRank]); NSNumber *remoteRank = @([other channelRank]); NSComparisonResult normalRank = [localRank compare:remoteRank]; NSComparisonResult invertedRank = NSInvertedComparisonResult(normalRank); if (invertedRank == NSOrderedSame) { return [self.user.nickname caseInsensitiveCompare:other.user.nickname]; } return invertedRank; } + (NSComparator)channelRankComparator { return [^(IRCChannelUser *object1, IRCChannelUser *object2) { return [object1 compareUsingRank:object2]; } copy]; } + (NSComparator)nicknameLengthComparator { return [^(IRCChannelUser *object1, IRCChannelUser *object2) { return (object1.user.nickname.length <= object2.user.nickname.length); } copy]; } - (NSString *)description { return [NSString stringWithFormat:@"", self.mark, self.user.nickname]; } - (BOOL)isEqual:(id)object { if (object == nil) { return NO; } if (object == self) { return YES; } if ([object isKindOfClass:[IRCChannelUser class]] == NO) { return NO; } IRCChannelUser *objectCast = (IRCChannelUser *)object; return (self.user == objectCast.user && ((self.modes == nil && objectCast.modes == nil) || [self.modes isEqualToString:objectCast.modes])); } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { IRCChannelUser *object = [self allocForCopyAsMutable:mutableCopy]; object->_user = self->_user; object->_modes = self->_modes; object->_creationTime = self->_creationTime; object->_incomingWeight = self->_incomingWeight; object->_outgoingWeight = self->_outgoingWeight; object->_lastWeightFade = self->_lastWeightFade; return [object initOnCopy]; } - (__kindof XRPortablePropertyObject *)mutableClass { return [IRCChannelUserMutable self]; } @end #pragma mark - @implementation IRCChannelUser (IRCUserRelationsPrivate) - (void)disassociateWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); [self.user disassociateUserWithChannel:channel]; } - (void)associateWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); [self.user associateUser:self withChannel:channel]; } @end #pragma mark - @implementation IRCChannelUserMutable @dynamic modes; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [IRCChannelUser self]; } - (void)setModes:(NSString *)modes { NSParameterAssert(modes != nil); if (self->_modes != modes) { self->_modes = [modes copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/Users/IRCUser.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TPCPreferencesLocal.h" #import "IRCClient.h" #import "IRCUserPersistentStorePrivate.h" #import "IRCUserRelationsPrivate.h" #import "IRCUserInternal.h" NS_ASSUME_NONNULL_BEGIN /* IRCUser has an internal timer that is started when relations reach zero. This timer runs for five minutes, using a GCD timer. When the timer fires, it removes the user from the client, thus remove any trace of it. */ #define _removeUserTimerInterval (60 * 5) // 5 minutes #define _presentAwayMessageFor301Threshold 300.0 @interface IRCUser () @property (nonatomic, weak, readwrite) IRCClient *client; @property (nonatomic, strong) IRCUserPersistentStore *persistentStore; @property (readonly) IRCUserRelations *relationsInt; @end @implementation IRCUser - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithNickname:(NSString *)nickname onClient:(IRCClient *)client { NSParameterAssert(nickname != nil); NSParameterAssert(client != nil); if ((self = [super init])) { self->_nickname = [nickname copy]; self.client = client; [self createNewPersistentStoreObject]; } return self; } - (void)createNewPersistentStoreObject { self.persistentStore = [IRCUserPersistentStore new]; self.persistentStore.relations = [IRCUserRelations new]; } - (void)dealloc { [self cancelRemoveUserTimer]; } - (IRCUserRelations *)relationsInt { return self.persistentStore.relations; } - (void)markAsAway { [self setIsAway:YES]; } - (void)markAsReturned { [self setIsAway:NO]; } - (void)setIsAway:(BOOL)isAway { if (self->_isAway != isAway) { self->_isAway = isAway; } } - (BOOL)presentAwayMessageFor301 { CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); if ((self.persistentStore.presentAwayMessageFor301LastEvent + _presentAwayMessageFor301Threshold) < now) { self.persistentStore.presentAwayMessageFor301LastEvent = now; return YES; } return NO; } - (nullable NSString *)hostmaskFragment { NSString *username = self.username; NSString *address = self.address; if (username == nil || address == nil) { return nil; } return [NSString stringWithFormat:@"%@@%@", username, address]; } - (nullable NSString *)hostmask { NSString *nickname = self.nickname; NSString *username = self.username; NSString *address = self.address; if (username == nil || address == nil) { return nil; } return [NSString stringWithFormat:@"%@!%@@%@", nickname, username, address]; } - (NSString *)banMask { NSString *nickname = self.nickname; NSString *username = self.username; NSString *address = self.address; if (username == nil || address == nil) { return [NSString stringWithFormat:@"%@!*@*", nickname]; } switch ([TPCPreferences banFormat]) { case TXHostmaskBanFormatWHNIN: { return [NSString stringWithFormat:@"*!*@%@", address]; } case TXHostmaskBanFormatWHAINN: { return [NSString stringWithFormat:@"*!%@@%@", username, address]; } case TXHostmaskBanFormatWHANNI: { return [NSString stringWithFormat:@"%@!*%@", nickname, address]; } case TXHostmaskBanFormatExact: { return [NSString stringWithFormat:@"%@!%@@%@", nickname, username, address]; } } return nil; } - (NSString *)lowercaseNickname { return self.nickname.lowercaseString; } - (NSString *)uppercaseNickname { return self.nickname.uppercaseString; } - (NSString *)description { return [NSString stringWithFormat:@"", self.nickname]; } - (BOOL)isEqual:(id)object { if (object == nil) { return NO; } if (object == self) { return YES; } if ([object isKindOfClass:[IRCUser class]] == NO) { return NO; } IRCUser *objectCast = (IRCUser *)object; return (self.client == objectCast.client && ((self.nickname == nil && objectCast.nickname == nil) || [self.nickname isEqualToString:objectCast.nickname]) && ((self.username == nil && objectCast.username == nil) || [self.username isEqualToString:objectCast.username]) && ((self.address == nil && objectCast.address == nil) || [self.address isEqualToString:objectCast.address]) && ((self.realName == nil && objectCast.realName == nil) || [self.realName isEqualToString:objectCast.realName]) && self.isAway == objectCast.isAway && self.isIRCop == objectCast.isIRCop); } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { IRCUser *object = [self allocForCopyAsMutable:mutableCopy]; object->_client = self->_client; object->_persistentStore = self->_persistentStore; object->_nickname = self->_nickname; object->_username = self->_username; object->_address = self->_address; object->_realName = self->_realName; object->_isAway = self->_isAway; object->_isIRCop = self->_isIRCop; return [object initOnCopy]; } - (__kindof XRPortablePropertyObject *)mutableClass { return [IRCUserMutable self]; } - (void)updateRemoveUserTimerBlockToFire { /* If the timer is already active, we reset the block that is scheduled so that the user that is targeted is always the primary */ dispatch_source_t removeUserTimer = self.persistentStore.removeUserTimer; if (removeUserTimer == nil) { return; } dispatch_block_t blockToFire = [self removeUserTimerBlockToFire]; dispatch_source_set_event_handler(removeUserTimer, blockToFire); } - (void)toggleRemoveUserTimer { if (self.relationsInt.numberOfRelations > 0) { [self cancelRemoveUserTimer]; } else { [self startRemoveUserTimer]; } } - (void)startRemoveUserTimer { dispatch_source_t removeUserTimer = self.persistentStore.removeUserTimer; if (removeUserTimer != NULL) { return; } dispatch_block_t blockToFire = [self removeUserTimerBlockToFire]; removeUserTimer = XRScheduleBlockOnGlobalQueue(blockToFire, _removeUserTimerInterval); XRResumeScheduledBlock(removeUserTimer); if (removeUserTimer == NULL) { LogToConsoleFault("Failed to create timer to remove user"); blockToFire(); // Remove user if timer isn't available return; } self.persistentStore.removeUserTimer = removeUserTimer; } - (void)cancelRemoveUserTimer { dispatch_source_t removeUserTimer = self.persistentStore.removeUserTimer; if (removeUserTimer == nil) { return; } XRCancelScheduledBlock(removeUserTimer); self.persistentStore.removeUserTimer = nil; } - (dispatch_block_t)removeUserTimerBlockToFire { /* Using weak references means that the object can be deallocated when the timer is active. -dealloc will cancel the timer if it is active. */ __weak IRCClient *client = self.client; __weak IRCUser *user = self; return [^{ [client removeUser:user]; } copy]; } @end #pragma mark - @implementation IRCUser (IRCUserRelationsPrivate) - (void)associateUser:(IRCChannelUser *)user withChannel:(IRCChannel *)channel { [self.relationsInt associateUser:user withChannel:channel]; [self toggleRemoveUserTimer]; } - (void)disassociateUserWithChannel:(IRCChannel *)channel { [self.relationsInt disassociateUserWithChannel:channel]; [self toggleRemoveUserTimer]; } - (nullable IRCChannelUser *)userAssociatedWithChannel:(IRCChannel *)channel { return [self.relationsInt userAssociatedWithChannel:channel]; } - (void)relinkRelations { NSArray *relatedUsers = self.relationsInt.relatedUsers; [relatedUsers makeObjectsPerformSelector:@selector(changeUserToUser:) withObject:self]; } - (void)becamePrimaryUser { [self updateRemoveUserTimerBlockToFire]; [self relinkRelations]; } - (void)enumerateRelations:(void (NS_NOESCAPE ^)(IRCChannel *channel, IRCChannelUser *member, BOOL *stop))block { [self.relationsInt enumerateRelations:block]; } @end #pragma mark - @implementation IRCUser (IRCUserRelations) - (NSDictionary *)relations { return self.relationsInt.relations; } @end #pragma mark - @implementation IRCUserMutable @dynamic nickname; @dynamic username; @dynamic address; @dynamic realName; @dynamic isAway; @dynamic isIRCop; - (instancetype)initWithClient:(IRCClient *)client { return [self initWithNickname:@"" onClient:client]; } + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [IRCUser self]; } - (void)setNickname:(NSString *)nickname { NSParameterAssert(nickname != nil); if (self->_nickname != nickname) { self->_nickname = [nickname copy]; } } - (void)setUsername:(nullable NSString *)username { if (self->_username != username) { self->_username = [username copy]; } } - (void)setAddress:(nullable NSString *)address { if (self->_address != address) { self->_address = [address copy]; } } - (void)setRealName:(nullable NSString *)realName { if (self->_realName != realName) { self->_realName = [realName copy]; } } - (void)setIsAway:(BOOL)isAway { if (self->_isAway != isAway) { self->_isAway = isAway; } } - (void)setIsIRCop:(BOOL)isIRCop { if (self->_isIRCop != isIRCop) { self->_isIRCop = isIRCop; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/Users/IRCUserNicknameColorStyleGenerator.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "TPCPreferencesUserDefaults.h" #import "TPCThemeController.h" #import "TPCTheme.h" #import "IRCUserNicknameColorStyleGeneratorPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _overridesDefaultsKey @"Nickname Color Style Overrides (v2)" @implementation IRCUserNicknameColorStyleGenerator + (NSString *)nicknameColorStyleForString:(NSString *)inputString { return [self nicknameColorStyleForString:inputString isOverride:NULL]; } + (NSString *)nicknameColorStyleForString:(NSString *)inputString isOverride:(BOOL * _Nullable)isOverride { NSParameterAssert(inputString != nil); NSString *unshuffledString = inputString.lowercaseString; NSColor *styleOverride = [self nicknameColorStyleOverrideForKey:unshuffledString]; if (styleOverride) { if (isOverride) { *isOverride = YES; } return styleOverride.hexadecimalValue; } else { if (isOverride) { *isOverride = NO; } } TPCThemeSettingsNicknameColorStyle colorStyle = themeSettings().nicknameColorStyle; NSNumber *stringHash = [self hashForString:unshuffledString colorStyle:colorStyle]; return [self nicknameColorStyleForHash:stringHash colorStyle:colorStyle]; } + (NSString *)nicknameColorStyleForHash:(NSNumber *)stringHash colorStyle:(TPCThemeSettingsNicknameColorStyle)colorStyle { NSParameterAssert(stringHash != nil); BOOL onLightBackground = (colorStyle == TPCThemeSettingsNicknameColorStyleLight); unsigned int stringHash32 = stringHash.intValue; int shash = (stringHash32 >> 1); int lhash = (stringHash32 >> 2); int h = (stringHash32 % 360); int s; int l; if (onLightBackground) { s = (shash % 50 + 35); // 35 - 85 l = (lhash % 38 + 20); // 20 - 58 // Lower lightness for Yellow, Green, Cyan if (h > 45 && h <= 195) { l = (lhash % 21 + 20); // 20 - 41 if (l > 31) { s = (shash % 40 + 55); // 55 - 95 } else { s = (shash % 35 + 65); // 65 - 95 } } // Give the reds a bit more saturation if (h <= 25 || h >= 335) { s = (shash % 33 + 45); // 45 - 78 } } else { s = (shash % 50 + 45); // 50 - 95 l = (lhash % 36 + 45); // 45 - 81 // give the pinks a wee bit more lightness if (h >= 280 && h < 335) { l = (lhash % 36 + 50); // 50 - 86 } // Give the blues a smaller (but lighter) range if (h >= 210 && h < 240) { l = (lhash % 30 + 60); // 60 - 90 } // Tone down very specific range of blue/purple if (h >= 240 && h < 280) { s = (shash % 55 + 40); // 40 - 95 l = (lhash % 20 + 65); // 65 - 85 } // Give the reds a bit less saturation if (h <= 25 || h >= 335) { s = (shash % 33 + 45); // 45 - 78 } // Give the yellows and greens a bit less saturation as well if (h >= 50 && h <= 150) { s = (shash % 50 + 40); // 40 - 90 } } return [NSString stringWithFormat:@"hsl(%i,%i%%,%i%%)", h, s, l]; } + (NSString *)preprocessString:(NSString *)inputString colorStyle:(TPCThemeSettingsNicknameColorStyle)colorStyle { NSParameterAssert(inputString != nil); return [NSString stringWithFormat:@"a-%@", inputString]; } + (NSNumber *)hashForString:(NSString *)inputString colorStyle:(TPCThemeSettingsNicknameColorStyle)colorStyle { NSParameterAssert(inputString != nil); NSString *stringToHash = [self preprocessString:inputString colorStyle:colorStyle]; NSData *stringToHashData = [stringToHash dataUsingEncoding:NSUTF8StringEncoding]; NSMutableData *hashedData = [NSMutableData dataWithLength:CC_MD5_DIGEST_LENGTH]; TEXTUAL_IGNORE_DEPRECATION_BEGIN CC_MD5(stringToHashData.bytes, (CC_LONG)stringToHashData.length, hashedData.mutableBytes); TEXTUAL_IGNORE_DEPRECATION_END unsigned int hashedValue; [hashedData getBytes:&hashedValue length:sizeof(unsigned int)]; return @(hashedValue); } /* * Color override storage talks in NSColor instead of hexadecimal strings for a few reasons: * 1. Easier to work with when modifying. No need to perform messy string conversion. * 2. Easier to change output format in another update (if that decision is made) */ + (void)migrateNicknameColorStyleOverrides { /* Migrate from database that used NSArchiver to one that uses NSKeyedArchiver. */ /* This migration is non-destructive to the legacy database. The data that is translated to NSKeyedUnarchiver is saved into a new defaults key. */ NSDictionary *legacyOverrides = [RZUserDefaults() dictionaryForKey:@"Nickname Color Style Overrides"]; NSMutableDictionary *newOverrides = [NSMutableDictionary dictionaryWithCapacity:legacyOverrides.count]; [legacyOverrides enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { TEXTUAL_IGNORE_DEPRECATION_BEGIN id override = [NSUnarchiver unarchiveObjectWithData:obj]; TEXTUAL_IGNORE_DEPRECATION_END if (override == nil || [override isKindOfClass:[NSColor class]] == NO) { LogToConsoleError("Failed to decode contents of '%{private}@'", key); return; } NSError *error; override = [NSKeyedArchiver archivedDataWithRootObject:override requiringSecureCoding:YES error:&error]; if (error) { LogToConsoleError("Failed to decode contents for '%{private}@': %{public}@", key, error.description); return; } [newOverrides setObject:override forKey:key]; }]; [RZUserDefaults() setObject:[newOverrides copy] forKey:_overridesDefaultsKey]; } + (nullable NSColor *)nicknameColorStyleOverrideForKey:(NSString *)styleKey { NSParameterAssert(styleKey != nil); NSDictionary *colorOverrides = [RZUserDefaults() dictionaryForKey:_overridesDefaultsKey]; if (colorOverrides == nil) { return nil; } id colorObject = colorOverrides[styleKey]; if ([colorObject isKindOfClass:[NSData class]] == NO) { return nil; } NSError *error; NSColor *colorValue = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSColor class] fromData:colorObject error:&error]; if (error) { LogToConsoleError("Failed to decode color for '%{private}@': %{public}@", styleKey, error.description); } return colorValue; } + (void)setNicknameColorStyleOverride:(nullable NSColor *)styleValue forKey:(NSString *)styleKey { NSParameterAssert(styleKey != nil); NSDictionary *colorOverrides = [RZUserDefaults() dictionaryForKey:_overridesDefaultsKey]; if (colorOverrides == nil && styleValue == nil) { return; } NSData *colorObject = nil; if (styleValue) { NSError *error; colorObject = [NSKeyedArchiver archivedDataWithRootObject:styleValue requiringSecureCoding:YES error:&error]; if (error) { LogToConsoleError("Failed to decode color for '%{private}@': %{public}@", styleKey, error.description); return; } if (colorOverrides == nil) { colorOverrides = [NSDictionary new]; } } NSMutableDictionary *colorOverridesNew = [colorOverrides mutableCopy]; if (styleValue == nil) { [colorOverridesNew removeObjectForKey:styleKey]; } else { colorOverridesNew[styleKey] = colorObject; } if (colorOverridesNew.count == 0) { [RZUserDefaults() removeObjectForKey:_overridesDefaultsKey]; } else { [RZUserDefaults() setObject:[colorOverridesNew copy] forKey:_overridesDefaultsKey]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/Users/IRCUserPersistentStore.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCUserPersistentStorePrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation IRCUserPersistentStore @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/IRC/Users/IRCUserRelations.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCChannel.h" #import "IRCUserRelationsPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface IRCUserRelations () @property (nonatomic, strong, nullable) NSMutableDictionary *relationsPrivate; @end @implementation IRCUserRelations - (NSDictionary *)relations { @synchronized (self.relationsPrivate) { if (self.relationsPrivate == nil) { return @{}; } return [self.relationsPrivate copy]; } } - (NSArray *)relatedChannels { @synchronized (self.relationsPrivate) { if (self.relationsPrivate == nil) { return @[]; } return self.relationsPrivate.allKeys; } } - (NSArray *)relatedUsers { @synchronized (self.relationsPrivate) { if (self.relationsPrivate == nil) { return @[]; } return self.relationsPrivate.allValues; } } - (void)enumerateRelations:(void (NS_NOESCAPE ^)(IRCChannel *channel, IRCChannelUser *member, BOOL *stop))block { @synchronized (self.relationsPrivate) { if (self.relationsPrivate == nil) { return; } [self.relationsPrivate enumerateKeysAndObjectsUsingBlock:block]; } } - (NSUInteger)numberOfRelations { @synchronized (self.relationsPrivate) { if (self.relationsPrivate == nil) { return 0; } return self.relationsPrivate.count; } } - (void)associateUser:(IRCChannelUser *)user withChannel:(IRCChannel *)channel { NSParameterAssert(user != nil); NSParameterAssert(channel != nil); if (channel.isChannel == NO) { return; } @synchronized (self.relationsPrivate) { if (self.relationsPrivate == nil) { self.relationsPrivate = [NSMutableDictionary dictionary]; } /* IRCChannel does not really support copying. It returns self. The protocol is declared here in a cast, instead of in the header for IRCChannel, so plugin author's don't make a mistake. */ self.relationsPrivate[(IRCChannel *)channel] = user; } } - (void)disassociateUserWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if (channel.isChannel == NO) { return; } @synchronized (self.relationsPrivate) { if (self.relationsPrivate == nil) { return; } [self.relationsPrivate removeObjectForKey:channel]; if (self.relationsPrivate.count == 0) { self.relationsPrivate = nil; } } } - (nullable IRCChannelUser *)userAssociatedWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if (channel.isChannel == NO) { return nil; } @synchronized (self.relationsPrivate) { if (self.relationsPrivate == nil) { return nil; } return self.relationsPrivate[channel]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/Color Formatting/IRCColorFormat.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TVCLogRenderer.h" #import "IRC.h" #import "IRCClientConfig.h" #import "IRCClient.h" #import "IRCColorFormatPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const IRCTextFormatterBoldAttributeName = @"IRCTextFormatterBoldAttributeName"; NSString * const IRCTextFormatterItalicAttributeName = @"IRCTextFormatterItalicAttributeName"; NSString * const IRCTextFormatterMonospaceAttributeName = @"IRCTextFormatterMonospaceAttributeName"; NSString * const IRCTextFormatterStrikethroughAttributeName = @"IRCTextFormatterStrikethroughAttributeName"; NSString * const IRCTextFormatterUnderlineAttributeName = @"IRCTextFormatterUnderlineAttributeName"; NSString * const IRCTextFormatterForegroundColorAttributeName = @"IRCTextFormatterForegroundColorAttributeName"; NSString * const IRCTextFormatterBackgroundColorAttributeName = @"IRCTextFormatterBackgroundColorAttributeName"; NSString * const IRCTextFormatterSpoilerAttributeName = @"IRCTextFormatterSpoilerAttributeName"; #pragma mark - #pragma mark Private Headers @interface NSMutableString (IRCTextFormatterPrivate) - (NSUInteger)wrapIRCTextFormatterResultWith:(NSUInteger)minimumIndex maxDistance:(NSUInteger)maxDistance; @end #pragma mark - #pragma mark Effects Container @interface IRCTextFormatterEffect () @property (nonatomic, assign, readwrite) IRCTextFormatterEffectType type; @property (nonatomic, copy, nullable, readwrite) NSString *value; @property (nonatomic, assign, readwrite) UniChar controlCharacter; @property (nonatomic, assign, readwrite) NSUInteger length; @end @interface IRCTextFormatterEffects () @property (nonatomic, copy, readwrite) NSArray *effects; @property (nonatomic, assign, readwrite) NSUInteger maximumLength; @end @implementation IRCTextFormatterEffect + (nullable instancetype)effectWithType:(IRCTextFormatterEffectType)type { return [[self alloc] initWithEffect:type withValue:nil]; } + (nullable instancetype)effectWithType:(IRCTextFormatterEffectType)type withValue:(nullable id)value { return [[self alloc] initWithEffect:type withValue:value]; } - (instancetype)init { return [self initWithEffect:IRCTextFormatterEffectNone withValue:nil]; } - (nullable instancetype)initWithEffect:(IRCTextFormatterEffectType)type { return [self initWithEffect:type withValue:nil]; } - (nullable instancetype)initWithEffect:(IRCTextFormatterEffectType)type withValue:(nullable id)value { if ((self = [super init])) { return [self _setupWithEffect:type withValue:value]; } return nil; } - (nullable instancetype)_setupWithEffect:(IRCTextFormatterEffectType)type withValue:(nullable id)value { UniChar controlCharacter = 0x00; NSUInteger valueLength = 0; NSString *valueOut = nil; switch (type) { case IRCTextFormatterEffectNone: { break; } case IRCTextFormatterEffectBold: { controlCharacter = IRCTextFormatterEffectBoldCharacter; valueLength = 2; // opening and closing break; } case IRCTextFormatterEffectItalic: { controlCharacter = IRCTextFormatterEffectItalicCharacter; valueLength = 2; // opening and closing break; } case IRCTextFormatterEffectMonospace: { controlCharacter = IRCTextFormatterEffectMonospaceCharacter; valueLength = 2; // opening and closing break; } case IRCTextFormatterEffectStrikethrough: { controlCharacter = IRCTextFormatterEffectStrikethroughCharacter; valueLength = 2; // opening and closing break; } case IRCTextFormatterEffectUnderline: { controlCharacter = IRCTextFormatterEffectUnderlineCharacter; valueLength = 2; // opening and closing break; } case IRCTextFormatterEffectForegroundColor: case IRCTextFormatterEffectBackgroundColor: { if ([value isKindOfClass:[NSColor class]]) { controlCharacter = IRCTextFormatterEffectColorAsHexCharacter; valueOut = [[value hexadecimalValue] substringFromIndex:1]; // Remove leading # } else if ([value isKindOfClass:[NSNumber class]]) { controlCharacter = IRCTextFormatterEffectColorAsDigitCharacter; valueOut = [value integerStringValueWithLeadingZero]; } if (valueOut == nil) { return nil; } if (type == IRCTextFormatterEffectForegroundColor) { valueLength = (valueOut.length + 2); // opening and closing } else { valueLength = (valueOut.length + 1); // leading comma } break; } default: { /* We return nil because all other formatters are just aliases. For example, spoiler is an alias for foreground and background. */ return nil; } } self.type = type; self.controlCharacter = controlCharacter; self.value = valueOut; self.length = valueLength; return self; } - (void)appendToStartOf:(NSMutableString *)string { NSParameterAssert(string != nil); IRCTextFormatterEffectType type = self.type; NSString *value = self.value; if (type == IRCTextFormatterEffectBackgroundColor) { [string appendFormat:@",%@", value]; return; } UniChar controlCharacter = self.controlCharacter; if (value == nil) { [string appendFormat:@"%c", controlCharacter]; } else { [string appendFormat:@"%c%@", controlCharacter, value]; } } - (void)appendToEndOf:(NSMutableString *)string { NSParameterAssert(string != nil); if (self.type == IRCTextFormatterEffectBackgroundColor) { return; } [string appendFormat:@"%c", self.controlCharacter]; } @end #pragma mark - @implementation IRCTextFormatterEffects + (instancetype)effectsInAttributes:(NSDictionary *)attributes { return [[self alloc] initWithAttributes:attributes]; } - (instancetype)init { return [self initWithAttributes:@{}]; } - (instancetype)initWithAttributes:(NSDictionary *)attributes { if ((self = [super init])) { return [self _setupWithAttributes:attributes]; } return nil; } - (instancetype)_setupWithAttributes:(NSDictionary *)attributes { NSUInteger maximumLength = 0; NSMutableArray *effects = [NSMutableArray arrayWithCapacity:7]; IRCTextFormatterEffect *foregroundColor = [IRCTextFormatterEffect effectWithType:IRCTextFormatterEffectForegroundColor withValue:attributes[IRCTextFormatterForegroundColorAttributeName]]; IRCTextFormatterEffect *backgroundColor = [IRCTextFormatterEffect effectWithType:IRCTextFormatterEffectBackgroundColor withValue:attributes[IRCTextFormatterBackgroundColorAttributeName]]; if (foregroundColor) { [effects addObject:foregroundColor]; maximumLength += foregroundColor.length; /* It's important that the background color ALWAYS follows the foreground color because the array will be enumerated in order to append the effects. */ /* Type of values must be the same. Can't mix and match integer color with hex. */ if (foregroundColor.controlCharacter == backgroundColor.controlCharacter) { [effects addObject:backgroundColor]; maximumLength += backgroundColor.length; } } BOOL textIsBold = [attributes boolForKey:IRCTextFormatterBoldAttributeName]; BOOL textIsItalicized = [attributes boolForKey:IRCTextFormatterItalicAttributeName]; BOOL textIsMonospace = [attributes boolForKey:IRCTextFormatterMonospaceAttributeName]; BOOL textIsStruckthrough = [attributes boolForKey:IRCTextFormatterStrikethroughAttributeName]; BOOL textIsUnderlined = [attributes boolForKey:IRCTextFormatterUnderlineAttributeName]; if (textIsBold) { IRCTextFormatterEffect *effect = [IRCTextFormatterEffect effectWithType:IRCTextFormatterEffectBold]; [effects addObject:effect]; maximumLength += effect.length; } if (textIsItalicized) { IRCTextFormatterEffect *effect = [IRCTextFormatterEffect effectWithType:IRCTextFormatterEffectItalic]; [effects addObject:effect]; maximumLength += effect.length; } if (textIsMonospace) { IRCTextFormatterEffect *effect = [IRCTextFormatterEffect effectWithType:IRCTextFormatterEffectMonospace]; [effects addObject:effect]; maximumLength += effect.length; } if (textIsStruckthrough) { IRCTextFormatterEffect *effect = [IRCTextFormatterEffect effectWithType:IRCTextFormatterEffectStrikethrough]; [effects addObject:effect]; maximumLength += effect.length; } if (textIsUnderlined) { IRCTextFormatterEffect *effect = [IRCTextFormatterEffect effectWithType:IRCTextFormatterEffectUnderline]; [effects addObject:effect]; maximumLength += effect.length; } self.effects = effects; self.maximumLength = maximumLength; return self; } - (void)appendToStartOf:(NSMutableString *)string { NSParameterAssert(string != nil); for (IRCTextFormatterEffect *effect in self.effects) { [effect appendToStartOf:string]; } } - (void)appendToEndOf:(NSMutableString *)string { NSParameterAssert(string != nil); /* Remember to use a reverse enumerator when closing because we need to close in the same order in which we opened. */ for (IRCTextFormatterEffect *effect in self.effects.reverseObjectEnumerator) { [effect appendToEndOf:string]; } } @end #pragma mark - #pragma mark Text Truncation @implementation NSAttributedString (IRCTextFormatterPrivate) - (NSString *)stringFormattedForChannel:(NSString *)channelName onClient:(IRCClient *)client withLineType:(TVCLogLineType)lineType effectiveRange:(NSRange * _Nullable)effectiveRange { NSParameterAssert(channelName != nil); NSParameterAssert(client != nil); /* ///////////////////////////////////////////////////// */ /* Server-side truncation does not count the total number of characters in the received message alone. It also counts everything that precedes it including hostmask, channel name, and command, and newline. Example: ":!@
PRIVMSG # :\r\n" The following math takes into account this information. Do not extend this method to support anything more than plain text messages such as PRIVMSG, ACTION, and NOTICE. */ /* ///////////////////////////////////////////////////// */ #define _textTruncationPRIVMSGCommandConstant 9 // "PRIVMSG" + surrounding spaces #define _textTruncationACTIONCommandConstant 17 // "PRIVMSG" + surrounding spaces + 0x01 + "ACTION" + 0x01 #define _textTruncationNOTICECommandConstant 8 // "NOTICE" + surrounding spaces #define _textTruncationHostmaskConstant 60 // Used if local hostmask is unknown /* Maximum distance from end of string that we will locate a character to perform wrapping on. */ #define _textTruncationWrapMaxDistance 25 /* Add length of colon (":") */ NSUInteger minimumLength = 1; /* Add length of hostmask */ NSString *userHostmask = client.userHostmask; if (userHostmask == nil) { minimumLength += _textTruncationHostmaskConstant; // It's better to have something rather than nothing } else { minimumLength += userHostmask.length; } /* Add length of command */ if (lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypePrivateMessageNoHighlight) { minimumLength += _textTruncationPRIVMSGCommandConstant; } else if (lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeActionNoHighlight) { minimumLength += _textTruncationACTIONCommandConstant; } else if (lineType == TVCLogLineTypeNotice) { minimumLength += _textTruncationNOTICECommandConstant; } else { NSAssert(NO, @"Line type not supported"); } /* Add length of channel name */ minimumLength += channelName.length; /* Add length of space trailing channel name and colon (" :") */ minimumLength += 2; /* Add length of trailing \r\n */ minimumLength += 2; /* Calculate maximum length */ NSUInteger maximumLength = TXMaximumIRCBodyLength; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 NSUInteger encryptionEstimate = [client lengthOfEncryptedMessageDirectedAt:channelName thatFitsWithinBounds:(maximumLength - minimumLength)]; if (encryptionEstimate > 0) { maximumLength = encryptionEstimate; } #endif /* Perform truncation */ NSString *string = self.string; NSStringEncoding encoding = client.config.primaryEncoding; NSMutableString *result = [NSMutableString string]; // Length of result with formatters __block NSUInteger resultLength = minimumLength; // Length of result without formatters __block NSUInteger deletionLength = 0; // Range of attribute segment being worked on NSRange segmentRange; // Maximum range to find next attribute segment within. // Defaults to string length because we don't know where // the first attribute segment may be until first pass. NSRange limitRange = NSMakeRange(0, string.length); /* Enumerate attributes */ while (limitRange.length > 0) { BOOL breakLoopAfterAppend = NO; /* ///////////////////////////////////////////////////// */ /* Gather information about the formatters and calculate the total number of bytes necessary to support them. */ /* ///////////////////////////////////////////////////// */ NSDictionary *attributes = [self attributesAtIndex:limitRange.location longestEffectiveRange:&segmentRange inRange:limitRange]; IRCTextFormatterEffects *formatters = [IRCTextFormatterEffects effectsInAttributes:attributes]; NSUInteger formattersLength = formatters.maximumLength; /* ///////////////////////////////////////////////////// */ /* Now that we know the minimum length and number of bytes for the formatters, we can start building the result. */ /* ///////////////////////////////////////////////////// */ /* At this point we do not care what the actual length of this segment is. The math only checks two things: Whether the formatter bytes found above will fit into this segment as well as at least one unicode character with a length of two. If neither of those can fit, then this segment is junk and we can break from it. */ /* If the location of this segment is 0, then we don't have to worry about checking the length yet. Since the formatter bytes will occupy at maximum X entries at location 0, we can do our append until the next, middle, or end segment. */ if (segmentRange.location > 0) { // Length calculations for the middle of our string. // Sally sold seashells down by the seashore. // |----------------------| <--- section we have to find NSUInteger newLength = (resultLength + // Length of what we have already formatted. formattersLength + // The formatter bytes for this segment. 2); // The sad little two. A single unicode character. /* Will this new segment exceed the maximum size? */ if (newLength > maximumLength) { break; } } /* Update math */ resultLength += formattersLength; /* Append formatter openers */ [formatters appendToStartOf:result]; /* We now go character by character and append that. We keep appending until the segment is completed or we run out of space. When that happens, we break. We have already added the formatter bytes into the math so any math checked in the loop will only be count towards the appended characters. */ for (NSUInteger i = 0; i < segmentRange.length;) { NSUInteger characterIndex = (segmentRange.location + i); /* While an emoji looks like only one character, it can be multiple bytes. We use -rangeOfComposedCharacterSequenceAtIndex: to know the true length of the character we are about to append. */ NSRange characterRange = [string rangeOfComposedCharacterSequenceAtIndex:characterIndex]; NSString *character = [string substringWithRange:characterRange]; /* Update math */ NSInteger characterSize = [character lengthOfBytesUsingEncoding:encoding]; if (characterSize == 0) { characterSize = characterRange.length; // Just incase... } resultLength += characterSize; /* Would this character go over the max length? */ if (resultLength > maximumLength) { /* Look for best character to wrap on */ NSUInteger indexDifference = [result wrapIRCTextFormatterResultWith:segmentRange.location maxDistance:_textTruncationWrapMaxDistance]; if (indexDifference != NSNotFound) { deletionLength -= indexDifference; } /* Break attribute enumeration using stater variable because we are in nested statements. */ breakLoopAfterAppend = YES; break; // Break instead of return so that we can close formatters } /* Only update if we aren't at max */ deletionLength += characterRange.length; i += characterRange.length; /* Perform append */ [result appendString:character]; } /* Close formatters */ [formatters appendToEndOf:result]; /* Break from enumeration */ if (breakLoopAfterAppend) { break; } /* Calculate next range to find an attribute segment within. */ NSUInteger segmentRangeNewLength = (string.length - deletionLength); if (segmentRangeNewLength <= 0) { break; } segmentRange.location = deletionLength; segmentRange.length = segmentRangeNewLength; limitRange = segmentRange; } // attribute enumeration /* Return length that can be deleted to occupy the result */ if ( effectiveRange) { *effectiveRange = NSMakeRange(0, deletionLength); } /* Debug information */ LogToConsoleDebug("Minimum length: %{public}ld; Final length: %{public}ld; Difference: %{public}ld;", minimumLength, resultLength, (maximumLength - resultLength)); #undef _textTruncationPRIVMSGCommandConstant #undef _textTruncationACTIONCommandConstant #undef _textTruncationNOTICECommandConstant #undef _textTruncationHostmaskConstant #undef _textTruncationWrapMaxDistance return result; } @end #pragma mark - @implementation NSMutableAttributedString (IRCTextFormatterPrivate) - (NSString *)stringFormattedForChannel:(NSString *)channelName onClient:(IRCClient *)client withLineType:(TVCLogLineType)lineType { NSParameterAssert(channelName != nil); NSParameterAssert(client != nil); NSRange effectiveRange; NSString *result = [self stringFormattedForChannel:channelName onClient:client withLineType:lineType effectiveRange:&effectiveRange]; [self deleteCharactersInRange:effectiveRange]; return result; } @end #pragma mark - @implementation NSAttributedString (IRCTextFormatter) #pragma mark - #pragma mark Text Truncation - (NSString *)stringFormattedForIRC { NSString *string = self.string; NSMutableString *result = [NSMutableString string]; [self enumerateAttributesInRange:self.range options:0 usingBlock:^(NSDictionary *attributes, NSRange effectiveRange, BOOL *stop) { IRCTextFormatterEffects *formatters = [IRCTextFormatterEffects effectsInAttributes:attributes]; [formatters appendToStartOf:result]; NSString *segment = [string substringWithRange:effectiveRange]; [result appendString:segment]; [formatters appendToEndOf:result]; }]; return result; } - (BOOL)IRCFormatterAttributeSetInRange:(IRCTextFormatterEffectType)effect range:(NSRange)limitRange { __block BOOL returnValue = NO; [self enumerateAttributesInRange:limitRange options:0 usingBlock:^(NSDictionary *attributes, NSRange effectiveRange, BOOL *stop) { switch (effect) { case IRCTextFormatterEffectNone: { break; } case IRCTextFormatterEffectBold: { if ([attributes boolForKey:IRCTextFormatterBoldAttributeName] == NO) { return; } returnValue = YES; *stop = YES; break; } case IRCTextFormatterEffectItalic: { if ([attributes boolForKey:IRCTextFormatterItalicAttributeName] == NO) { return; } returnValue = YES; *stop = YES; break; } case IRCTextFormatterEffectMonospace: { if ([attributes boolForKey:IRCTextFormatterMonospaceAttributeName] == NO) { return; } returnValue = YES; *stop = YES; break; } case IRCTextFormatterEffectUnderline: { if ([attributes boolForKey:IRCTextFormatterUnderlineAttributeName] == NO) { return; } returnValue = YES; *stop = YES; break; } case IRCTextFormatterEffectStrikethrough: { if ([attributes boolForKey:IRCTextFormatterStrikethroughAttributeName] == NO) { return; } returnValue = YES; *stop = YES; break; } case IRCTextFormatterEffectForegroundColor: { id foregroundColor = attributes[IRCTextFormatterForegroundColorAttributeName]; if (foregroundColor == nil) { return; } if ([foregroundColor isKindOfClass:[NSNumber class]]) { NSInteger colorCode = [foregroundColor integerValue]; if (colorCode < 0 || colorCode > IRCTextFormatterEffectColorHighestDigit) { return; } } else if ([foregroundColor isKindOfClass:[NSColor class]] == NO) { return; } returnValue = YES; *stop = YES; break; } case IRCTextFormatterEffectBackgroundColor: { id backgroundColor = attributes[IRCTextFormatterBackgroundColorAttributeName]; if (backgroundColor == nil) { return; } if ([backgroundColor isKindOfClass:[NSNumber class]]) { NSInteger colorCode = [backgroundColor integerValue]; if (colorCode < 0 || colorCode > IRCTextFormatterEffectColorHighestDigit) { return; } } else if ([backgroundColor isKindOfClass:[NSColor class]] == NO) { return; } returnValue = YES; *stop = YES; break; } case IRCTextFormatterEffectSpoiler: { if ([attributes boolForKey:IRCTextFormatterSpoilerAttributeName] == NO) { return; } returnValue = YES; *stop = YES; break; } } }]; return returnValue; } @end #pragma mark - #pragma mark Adding/Removing Formatting @implementation NSMutableAttributedString (IRCTextFormatter) - (void)setIRCFormatterAttribute:(IRCTextFormatterEffectType)effect value:(id)value range:(NSRange)limitRange { [self enumerateAttributesInRange:limitRange options:NSAttributedStringEnumerationReverse usingBlock:^(NSDictionary *attributes, NSRange effectiveRange, BOOL *stop) { NSFont *baseFont = attributes[NSFontAttributeName]; switch (effect) { case IRCTextFormatterEffectNone: { break; } case IRCTextFormatterEffectBold: { if ([baseFont fontTraitSet:NSBoldFontMask] == NO) { baseFont = [RZFontManager() convertFont:baseFont toHaveTrait:NSBoldFontMask]; } if (baseFont) { [self addAttribute:IRCTextFormatterBoldAttributeName value:@(YES) range:effectiveRange]; [self addAttribute:NSFontAttributeName value:baseFont range:effectiveRange]; } break; } case IRCTextFormatterEffectItalic: { if ([baseFont fontTraitSet:NSItalicFontMask] == NO) { baseFont = [RZFontManager() convertFont:baseFont toHaveTrait:NSItalicFontMask]; } if (baseFont) { [self addAttribute:IRCTextFormatterItalicAttributeName value:@(YES) range:effectiveRange]; [self addAttribute:NSFontAttributeName value:baseFont range:effectiveRange]; } break; } case IRCTextFormatterEffectMonospace: { baseFont = [RZFontManager() convertFont:baseFont toFamily:@"Menlo"]; [self addAttribute:IRCTextFormatterMonospaceAttributeName value:@(YES) range:effectiveRange]; [self addAttribute:NSFontAttributeName value:baseFont range:effectiveRange]; break; } case IRCTextFormatterEffectUnderline: { [self addAttribute:IRCTextFormatterUnderlineAttributeName value:@(YES) range:effectiveRange]; [self addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:effectiveRange]; break; } case IRCTextFormatterEffectStrikethrough: { [self addAttribute:IRCTextFormatterStrikethroughAttributeName value:@(YES) range:effectiveRange]; [self addAttribute:NSStrikethroughStyleAttributeName value:@(NSUnderlineStyleSingle) range:effectiveRange]; break; } case IRCTextFormatterEffectForegroundColor: { if (value == nil) { break; } if ([value isKindOfClass:[NSNumber class]]) { NSInteger colorCode = [value integerValue]; if (colorCode >= 0 && colorCode <= IRCTextFormatterEffectColorHighestDigit) { [self addAttribute:IRCTextFormatterForegroundColorAttributeName value:@(colorCode) range:effectiveRange]; [self addAttribute:NSForegroundColorAttributeName value:[TVCLogRenderer mapColorCode:colorCode] range:effectiveRange]; } } else if ([value isKindOfClass:[NSColor class]]) { [self addAttribute:IRCTextFormatterForegroundColorAttributeName value:value range:effectiveRange]; [self addAttribute:NSForegroundColorAttributeName value:value range:effectiveRange]; } break; } case IRCTextFormatterEffectBackgroundColor: { if (value == nil) { break; } if ([value isKindOfClass:[NSNumber class]]) { NSInteger colorCode = [value integerValue]; if (colorCode >= 0 && colorCode <= IRCTextFormatterEffectColorHighestDigit) { [self addAttribute:IRCTextFormatterBackgroundColorAttributeName value:@(colorCode) range:effectiveRange]; [self addAttribute:NSBackgroundColorAttributeName value:[TVCLogRenderer mapColorCode:colorCode] range:effectiveRange]; } } else if ([value isKindOfClass:[NSColor class]]) { [self addAttribute:IRCTextFormatterBackgroundColorAttributeName value:value range:effectiveRange]; [self addAttribute:NSBackgroundColorAttributeName value:value range:effectiveRange]; } break; } case IRCTextFormatterEffectSpoiler: { [self addAttribute:IRCTextFormatterSpoilerAttributeName value:value range:effectiveRange]; break; } } }]; } - (void)removeIRCFormatterAttribute:(IRCTextFormatterEffectType)effect range:(NSRange)limitRange { [self enumerateAttributesInRange:limitRange options:NSAttributedStringEnumerationReverse usingBlock:^(NSDictionary *attributes, NSRange effectiveRange, BOOL *stop) { NSFont *baseFont = attributes[NSFontAttributeName]; if (baseFont == nil) { return; } switch (effect) { case IRCTextFormatterEffectNone: { break; } case IRCTextFormatterEffectBold: { if ([baseFont fontTraitSet:NSBoldFontMask]) { baseFont = [RZFontManager() convertFont:baseFont toNotHaveTrait:NSBoldFontMask]; if (baseFont) { [self addAttribute:NSFontAttributeName value:baseFont range:effectiveRange]; } [self removeAttribute:IRCTextFormatterBoldAttributeName range:effectiveRange]; } break; } case IRCTextFormatterEffectItalic: { if ([baseFont fontTraitSet:NSItalicFontMask]) { baseFont = [RZFontManager() convertFont:baseFont toNotHaveTrait:NSItalicFontMask]; if (baseFont) { [self addAttribute:NSFontAttributeName value:baseFont range:effectiveRange]; } [self removeAttribute:IRCTextFormatterItalicAttributeName range:effectiveRange]; } break; } case IRCTextFormatterEffectMonospace: { [self removeAttribute:NSFontAttributeName range:effectiveRange]; [self removeAttribute:IRCTextFormatterMonospaceAttributeName range:effectiveRange]; break; } case IRCTextFormatterEffectUnderline: { [self removeAttribute:NSUnderlineStyleAttributeName range:effectiveRange]; [self removeAttribute:IRCTextFormatterUnderlineAttributeName range:effectiveRange]; break; } case IRCTextFormatterEffectStrikethrough: { [self removeAttribute:NSStrikethroughStyleAttributeName range:effectiveRange]; [self removeAttribute:IRCTextFormatterStrikethroughAttributeName range:effectiveRange]; break; } case IRCTextFormatterEffectForegroundColor: { [self removeAttribute:NSBackgroundColorAttributeName range:effectiveRange]; [self removeAttribute:IRCTextFormatterForegroundColorAttributeName range:effectiveRange]; break; } case IRCTextFormatterEffectBackgroundColor: { [self removeAttribute:NSBackgroundColorAttributeName range:effectiveRange]; [self removeAttribute:IRCTextFormatterBackgroundColorAttributeName range:effectiveRange]; break; } case IRCTextFormatterEffectSpoiler: { [self removeAttribute:IRCTextFormatterSpoilerAttributeName range:effectiveRange]; break; } } }]; } @end #pragma mark - #pragma mark Truncation Helpers @implementation NSMutableString (IRCTextFormatterPrivate) /* Look for best character to wrap on */ /* Now this is where the append gets a little technical. We want clean truncation. Not half-assed ones. Therefore, if we have space character and it is within a certain range of the end of the line, then we will stop append at that instead of breaking inside of a word. */ /* Returns number of characters deleted from self or NSNotFound if none. */ /* minimumIndex is index we can't pass so that we always wrap within our own segment and not within another. */ /* maxDistance is how far back we search backwards from the end. While similar, this value is different compared to minimumIndex. maxDistance is a suggestion whereas minimumIndex is a must. */ - (NSUInteger)wrapIRCTextFormatterResultWith:(NSUInteger)minimumIndex maxDistance:(NSUInteger)maxDistance { NSParameterAssert(maxDistance > 0); NSUInteger selfLength = self.length; NSUInteger searchIndex = ((self.length - 1) - maxDistance); NSRange searchRange = NSMakeRange(searchIndex, maxDistance); NSRange spaceRange = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] options:NSBackwardsSearch range:searchRange]; if (spaceRange.location == NSNotFound || spaceRange.location < minimumIndex) { return NSNotFound; } NSInteger indexDifference = (selfLength - spaceRange.location); [self deleteCharactersInRange:NSMakeRange(spaceRange.location, indexDifference)]; return indexDifference; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/External Libraries/Sockets/OELReachability.m ================================================ /* Copyright (c) 2011, Tony Million. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import #import "OELReachability.h" NS_ASSUME_NONNULL_BEGIN @interface OELReachability () @property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef; - (void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; @end static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) { OELReachability *reachability = ((__bridge OELReachability *)info); [reachability reachabilityChanged:flags]; } @implementation OELReachability + (nullable OELReachability *)reachabilityForInternetConnection { SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, "www.google.com"); if (ref) { return [[self alloc] initWithReachabilityRef:ref]; } return nil; } - (OELReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref { if ((self = [super init])) { self.reachabilityRef = ref; } return self; } - (void)dealloc { [self stopNotifier]; if (self.reachabilityRef) { CFRelease(self.reachabilityRef); self.reachabilityRef = nil; } self.reachableBlock = nil; self.unreachableBlock = nil; } - (BOOL)startNotifier { SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; if (SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context)) { if (SCNetworkReachabilityScheduleWithRunLoop(self.reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)) { return YES; } } return NO; } - (void)stopNotifier { SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL); SCNetworkReachabilityUnscheduleFromRunLoop(self.reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); } - (BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags { if ((flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable) { return YES; } else { return NO; } } - (BOOL)isReachable { SCNetworkReachabilityFlags flags; if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags) == FALSE) { return NO; } return [self isReachableWithFlags:flags]; } - (void)reachabilityChanged:(SCNetworkReachabilityFlags)flags { if ([self isReachableWithFlags:flags]) { if (self.reachableBlock) { self.reachableBlock(self); } } else { if (self.unreachableBlock) { self.unreachableBlock(self); } } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/License Manager/Standalone/TLOLicenseManager.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPathInfo.h" #import "TPCPreferencesUserDefaults.h" #import "TLOLicenseManagerPrivate.h" NS_ASSUME_NONNULL_BEGIN /* * * TLOLicenseManager is designed to enforce remote license signatures, but * Textual is open source so it does not make much sense to design it in such * a way that tries to prevent copying. * * The license manager is designed to work in very specific ways: * * 1. Given a documented public key, then the public key is used to verify the * signature that is present in license files. * * 2. A hash of the public key is hardcoded into a function. On launch, the hash * of the expected public key and the activate public key are compared. If they * are not equal, then this copy of Textual is not "Genuine" — in this case, the * user is presented a small prompt informing that Textual is open source and they * should prefer the open source version over a possibly pirated version. * * 3. At no time shall the license manager make an attempt to lock a user out of * application. At most, limit functionality to trial-mode level. * */ /* * This source file does not contain source code from, but is designed * around concepts of, the open source project known as "AquaticPrime" * * */ #pragma mark - #pragma mark Private Implementation #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 static SecKeyRef TLOLicenseManagerPublicKey = NULL; static BOOL TLOLicenseManagerPublicKeyIsGenuineResult = YES; static NSDictionary * _Nullable TLOLicenseManagerCachedLicenseDictionary = nil; NSString * const TLOLicenseManagerHashOfGenuinePublicKey = @"2e14fea44d634095e91a8048404c4ea90d9487a0676f48e89b846a6926b2f4c1"; NSString * const TLOLicenseManagerLicenseKeyRegularExpression = @"^([a-z]{1,12})\\-([a-z]{1,12})\\-([a-z]{1,12})\\-([0-9]{1,35})$"; NSUInteger const TLOLicenseManagerLicenseKeyExpectedLength = 45; NSInteger const TLOLicenseManagerTrialModeMaximumLifespan = (-2592000); // 30 days in seconds NSUInteger const TLOLicenseManagerCurrentLicenseGeneration = 1; void TLOLicenseManagerDeleteLicenseFileIfBlacklisted(void); BOOL TLOLicenseManagerLicenseKeyBlacklisted(NSString *licenseKey); BOOL TLOLicenseManagerLicenseDictionaryIsValid(NSDictionary *licenseDictionary); void TLOLicenseManagerPopulatePublicKeyRef(void); void TLOLicenseManagerSetPublicKeyIsGenuine(void); BOOL TLOLicenseManagerLicenseFileExists(void); CFDataRef TLOLicenseManagerExportContentsOfKeyRef(SecKeyRef theKeyRef, BOOL isPublicKey); NSData * _Nullable TLOLicenseManagerPublicKeyContents(void); NSData * _Nullable TLOLicenseManagerLicenseFileContents(void); TLOLicenseManagerActionResult TLOLicenseManagerLoadLicenseDictionary(void); TLOLicenseManagerActionResult TLOLicenseManagerLoadLicenseDictionaryWithData(NSData *licenseContents); NSDictionary * _Nullable TLOLicenseManagerLicenseDictionary(void); NSDictionary * _Nullable TLOLicenseManagerLicenseDictionaryWithData(NSData *licenseContents); NSURL * _Nullable TLOLicenseManagerTrialModeInformationFilePath(void); void TLOLicenseManagerMigrateLicenseFiles(void); NSURL * _Nullable TLOLicenseManagerLicenseFilePath(void); NSNumberFormatter *TLOLicenseManagerStringValueNumberFormatter(void); NSString *TLOLicenseManagerStringValueForObject(id object); NSString * const TLOLicenseManagerLicenseDictionaryKeyCreationDate = @"licenseCreationDate"; NSString * const TLOLicenseManagerLicenseDictionaryKeyGeneration = @"licenseGeneration"; NSString * const TLOLicenseManagerLicenseDictionaryKeyLicenseKey = @"licenseKey"; NSString * const TLOLicenseManagerLicenseDictionaryKeyProductName = @"licenseProductName"; NSString * const TLOLicenseManagerLicenseDictionaryKeyOwnerContactAddress = @"licenseOwnerContactAddress"; NSString * const TLOLicenseManagerLicenseDictionaryKeyOwnerName = @"licenseOwnerName"; NSString * const TLOLicenseManagerLicenseDictionaryKeySignature = @"licenseSignature"; NSString * const TLOLicenseManagerLicenseDictionaryKeySignatureGeneration = @"licenseSignatureGeneration"; #endif #pragma mark - #pragma mark Implementation #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 void TLOLicenseManagerSetup(void) { static BOOL _setupComplete = NO; if (_setupComplete == NO) { _setupComplete = YES; TLOLicenseManagerPopulatePublicKeyRef(); TLOLicenseManagerMigrateLicenseFiles(); (void)TLOLicenseManagerLoadLicenseDictionary(); XRPerformBlockAsynchronouslyOnGlobalQueue(^{ TLOLicenseManagerSetPublicKeyIsGenuine(); TLOLicenseManagerDeleteLicenseFileIfBlacklisted(); }); } } #pragma mark - #pragma mark Trial Mode BOOL TLOLicenseManagerTextualIsRegistered(void) { if (TLOLicenseManagerPublicKeyIsGenuineResult == NO) { return NO; } else if (TLOLicenseManagerLicenseFileExists() == NO) { return NO; } NSDictionary *licenseDictionary = TLOLicenseManagerLicenseDictionary(); return (licenseDictionary != nil); } BOOL TLOLicenseManagerIsTrialExpired(void) { NSTimeInterval timeLeft = TLOLicenseManagerTimeRemainingTrial(); return (timeLeft >= 0); } NSTimeInterval TLOLicenseManagerTimeRemainingTrial(void) { /* Determine where trial information will be stored on disk. */ NSURL *trialInformationFilePath = TLOLicenseManagerTrialModeInformationFilePath(); if (trialInformationFilePath == nil) { return 0; } /* If the trial information file does not exist yet, then create a new one which will define when the trial period began. */ /* NSPropertyListSerialization is used by this function, in place of the built in NSData read & write methods, for better error reporting. */ if ([RZFileManager() fileExistsAtURL:trialInformationFilePath] == NO) { NSDictionary *trialInformation = @{ @"trialPeriodStartDate" : [NSDate date] }; NSError *trialInformationPropertyListError = nil; NSData *trialInformationPropertyList = [NSPropertyListSerialization dataWithPropertyList:trialInformation format:NSPropertyListBinaryFormat_v1_0 options:0 error:&trialInformationPropertyListError]; if (trialInformationPropertyList == nil) { LogToConsoleError("Failed to create trial information property list: %{public}@", trialInformationPropertyListError.localizedDescription); return 0; // Cannot continue function... } NSError *trialInformationWriteError = nil; if ([trialInformationPropertyList writeToURL:trialInformationFilePath options:NSDataWritingAtomic error:&trialInformationWriteError] == NO) { LogToConsoleError("Failed to write trial information to disk: %{public}@", trialInformationWriteError.localizedDescription); return 0; // Cannot continue function... } NSError *modifyTrialInformationAttributesError = nil; if ([trialInformationFilePath setResourceValue:@(YES) forKey:NSURLIsHiddenKey error:&modifyTrialInformationAttributesError] == NO) { LogToConsoleError("Failed to modify attributes of trial information file: %{public}@", modifyTrialInformationAttributesError.localizedDescription); return 0; // Cannot continue function... } NSError *lockTrialInformationFileError = nil; if ([RZFileManager() lockItemAtPath:trialInformationFilePath.path error:&lockTrialInformationFileError] == NO) { LogToConsoleError("Failed to lock the trial information file: %{public}@", lockTrialInformationFileError.localizedDescription); return 0; // Cannot continue function... } } /* Read trial information from disk. */ NSError *trialInformationDataReadError = nil; NSData *trialInformationData = [NSData dataWithContentsOfURL:trialInformationFilePath options:0 error:&trialInformationDataReadError]; if (trialInformationData == nil) { LogToConsoleError("Failed to read contents of trial information file: %{public}@", trialInformationDataReadError.localizedDescription); return 0; // Cannot continue function... } NSError *trialInformationPropertyListError = nil; NSDictionary *trialInformation = [NSPropertyListSerialization propertyListWithData:trialInformationData options:NSPropertyListImmutable format:NULL error:&trialInformationPropertyListError]; if (trialInformation == nil) { LogToConsoleError("Failed to convert property list to NSDictionary: %{public}@", trialInformationPropertyListError.localizedDescription); return 0; // Cannot continue function... } /* Given dictionary, get start date of trial and return time left. */ NSDate *trialPeriodStartDate = trialInformation[@"trialPeriodStartDate"]; if (trialPeriodStartDate == nil || [trialPeriodStartDate isKindOfClass:[NSDate class]] == NO) { LogToConsoleError("The value of 'trialPeriodStartDate' is nil or not of kind 'NSDate'"); return 0; // Cannot continue function... } /* trialPeriodStartInterval will be negative because it is in the past. */ NSTimeInterval trialPeriodStartInterval = trialPeriodStartDate.timeIntervalSinceNow; if (trialPeriodStartInterval > 0) { /* Return expired date for those who try to be clever by setting future time. */ return 0; } else if (trialPeriodStartInterval < TLOLicenseManagerTrialModeMaximumLifespan) { return 0; } else { return (TLOLicenseManagerTrialModeMaximumLifespan - trialPeriodStartInterval); } } NSURL * _Nullable TLOLicenseManagerTrialModeInformationFilePath(void) { NSURL *sourceURL = [TPCPathInfo applicationSupportURL]; if (sourceURL == nil) { return nil; } NSURL *baseURL = [sourceURL URLByAppendingPathComponent:@"/Textual_Trial_Information_v2.plist"]; return baseURL; } #pragma mark - #pragma mark User License File Validation BOOL TLOLicenseManagerLicenseKeyIsValid(NSString *licenseKey) { NSCParameterAssert(licenseKey != nil); if (licenseKey.length != TLOLicenseManagerLicenseKeyExpectedLength) { return NO; } if ([XRRegularExpression string:licenseKey isMatchedByRegex:TLOLicenseManagerLicenseKeyRegularExpression withoutCase:YES] == NO) { return NO; } else { return YES; } } NSNumberFormatter *TLOLicenseManagerStringValueNumberFormatter(void) { /* Regardless of whether a number in returned dictionary is an integer or double, it is formatted in the signature with four fraction digits. This allows us to avoid having to determine what type of number NSNumber is actually storing. Just format everything the same exact way. Integer 1 = "1.0000", Double 2.43561 = "2.4356" */ static NSNumberFormatter *numberFormatter = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ numberFormatter = [NSNumberFormatter new]; numberFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; numberFormatter.numberStyle = NSNumberFormatterDecimalStyle; numberFormatter.alwaysShowsDecimalSeparator = YES; numberFormatter.usesGroupingSeparator = NO; numberFormatter.minimumFractionDigits = 4; numberFormatter.maximumFractionDigits = 4; }); return numberFormatter; } NSString *TLOLicenseManagerStringValueForObject(id object) { /* The license dictionary can contain more than strings as of Textual 7. The license signature is the sum of all objects in the license dictionary, in alphabetical order, concatenated as strings. */ /* This method returns the string representation of an object. */ if ([object isKindOfClass:[NSArray class]]) { NSMutableString *stringValue = [NSMutableString new]; [object enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { NSString *objectValue = TLOLicenseManagerStringValueForObject(object); [stringValue appendFormat:@"%lu%@", index, objectValue]; }]; return [stringValue copy]; } else if ([object isKindOfClass:[NSDictionary class]]) { NSMutableString *stringValue = [NSMutableString new]; NSArray *sortedDictionaryKeys = [[object allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; for (NSString *key in sortedDictionaryKeys) { NSString *objectValue = TLOLicenseManagerStringValueForObject(object[key]); [stringValue appendFormat:@"%@%@", key, objectValue]; } return [stringValue copy]; } else if ([object isKindOfClass:[NSString class]]) { return object; } else if ([object isKindOfClass:[NSNumber class]]) { if ([object isBooleanValue]) { if ([object boolValue]) { return @"YES"; } else { return @"NO"; } } else { NSNumberFormatter *numberFormatter = TLOLicenseManagerStringValueNumberFormatter(); return [numberFormatter stringFromNumber:object]; } } return @""; } TLOLicenseManagerActionResult TLOLicenseManagerVerifyLicenseSignatureWithDictionary(NSDictionary *licenseDictionary) { NSCParameterAssert(licenseDictionary != nil); /* Attempt to populate public key information. */ if (TLOLicenseManagerPublicKey == NULL) { return TLOLicenseManagerActionResultOther; } /* Retrieve license signature information */ NSData *licenseSignature = licenseDictionary[TLOLicenseManagerLicenseDictionaryKeySignature]; if (licenseSignature == nil) { LogToConsoleError("Missing license signature in license dictionary"); return TLOLicenseManagerActionResultMalformedData; } /* Retrieve license generation */ NSUInteger licenseGeneration = [licenseDictionary unsignedIntegerForKey:TLOLicenseManagerLicenseDictionaryKeyGeneration]; if (licenseGeneration < TLOLicenseManagerCurrentLicenseGeneration) { return TLOLicenseManagerActionResultGenerationPrevious; } else if (licenseGeneration > TLOLicenseManagerCurrentLicenseGeneration) { return TLOLicenseManagerActionResultGenerationNext; } CFDataRef cfLicenseSignature = (__bridge CFDataRef)(licenseSignature); /* Combine all contents of the dictionary, in sorted order, excluding the license dictionary signature because thats used for comparison. */ NSMutableDictionary *licenseDictionaryToCombine = [licenseDictionary mutableCopy]; [licenseDictionaryToCombine removeObjectForKey:TLOLicenseManagerLicenseDictionaryKeySignature]; [licenseDictionaryToCombine removeObjectForKey:TLOLicenseManagerLicenseDictionaryKeySignatureGeneration]; NSString *combinedLicenseDataString = TLOLicenseManagerStringValueForObject(licenseDictionaryToCombine); if (combinedLicenseDataString.length <= 0) { LogToConsoleError("Length of combinedLicenseDataString is below or equal to zero (0)"); return TLOLicenseManagerActionResultMalformedData; } NSData *combinedLicenseData = [combinedLicenseDataString dataUsingEncoding:NSUTF8StringEncoding]; CFDataRef cfCombinedLicenseData = (__bridge CFDataRef)(combinedLicenseData); /* Setup transform function for verifying signature */ SecTransformRef verifyFunction = SecVerifyTransformCreate(TLOLicenseManagerPublicKey, cfLicenseSignature, NULL); if (verifyFunction == NULL) { LogToConsoleError("Failed to create transform using SecVerifyTransformCreate()"); return TLOLicenseManagerActionResultInvalidSignature; } /* Setup transform attributes */ if (SecTransformSetAttribute(verifyFunction, kSecTransformInputAttributeName, cfCombinedLicenseData, NULL) == false || SecTransformSetAttribute(verifyFunction, kSecDigestTypeAttribute, kSecDigestSHA2, NULL) == false || SecTransformSetAttribute(verifyFunction, kSecDigestLengthAttribute, (__bridge CFNumberRef)@(256), NULL) == false) { CFRelease(verifyFunction); LogToConsoleError("Failed to modify transform attributes using SecTransformSetAttribute()"); return TLOLicenseManagerActionResultInvalidSignature; } /* Perform signature verification */ CFTypeRef cfVerifyResult = SecTransformExecute(verifyFunction, NULL); CFRelease(verifyFunction); BOOL verifyResult = NO; if (CFGetTypeID(cfVerifyResult) == CFBooleanGetTypeID()) { verifyResult = (cfVerifyResult == kCFBooleanTrue); } else { LogToConsoleError("SecTransformExecute() returned a result that is not of type: CFBooleanRef"); } if (cfVerifyResult != NULL) { CFRelease(cfVerifyResult); } if (verifyResult == NO) { return TLOLicenseManagerActionResultInvalidSignature; } return TLOLicenseManagerActionResultSuccess; } #pragma mark - #pragma mark Reading & Writing User License File void TLOLicenseManagerMigrateLicenseFiles(void) { NSURL *newLocation = [TPCPathInfo applicationSupportURL]; NSURL *oldTrialData = [[TPCPathInfo userHomeURL] URLByAppendingPathComponent:@"/Library/Application Support/Textual/Textual_Trial_Information_v2.plist"]; NSURL *newTrialData = [newLocation URLByAppendingPathComponent:@"Textual_Trial_Information_v2.plist"]; NSError *moveError = nil; if ([RZFileManager() fileExistsAtURL:newTrialData] == NO && [RZFileManager() fileExistsAtURL:oldTrialData]) { if ([RZFileManager() copyItemAtURL:oldTrialData toURL:newTrialData error:&moveError]) { LogToConsole("Moved trial data file to new location"); } else { LogToConsoleError("Moving trial data file to new location failed: %{public}@", moveError.localizedDescription); moveError = nil; // nil out for next move } } NSURL *oldLicenseData = [[TPCPathInfo userHomeURL] URLByAppendingPathComponent:@"/Library/Application Support/Textual/Textual_User_License_v2.plist"]; NSURL *newLicenseData = [newLocation URLByAppendingPathComponent:@"Textual_User_License_v2.plist"]; if ([RZFileManager() fileExistsAtURL:newLicenseData] == NO && [RZFileManager() fileExistsAtURL:oldLicenseData]) { if ([RZFileManager() copyItemAtURL:oldLicenseData toURL:newLicenseData error:&moveError]) { LogToConsole("Moved license data file to new location"); } else { LogToConsoleError("Moving license data file to new location failed: %{public}@", moveError.localizedDescription); } } } NSURL * _Nullable TLOLicenseManagerLicenseFilePath(void) { NSURL *sourceURL = [TPCPathInfo applicationSupportURL]; if (sourceURL == nil) { return nil; } NSURL *baseURL = [sourceURL URLByAppendingPathComponent:@"/Textual_User_License_v2.plist"]; return baseURL; } TLOLicenseManagerActionResult TLOLicenseManagerDeleteLicenseFile(void) { return TLOLicenseManagerWriteLicenseFileContents(nil); } TLOLicenseManagerActionResult TLOLicenseManagerWriteLicenseFileContents(NSData * _Nullable newContents) { NSURL *licenseFilePath = TLOLicenseManagerLicenseFilePath(); if (newContents) { TLOLicenseManagerActionResult loadResult = TLOLicenseManagerLoadLicenseDictionaryWithData(newContents); if (loadResult != TLOLicenseManagerActionResultSuccess) { LogToConsoleError("Verify for new license file contents failed"); return loadResult; } NSError *writeFileError = nil; if ([newContents writeToURL:licenseFilePath options:NSDataWritingAtomic error:&writeFileError] == NO) { LogToConsoleError("Failed to write user license file with error: %{public}@", writeFileError.localizedDescription); return TLOLicenseManagerActionResultCannotWrite; } } else { TLOLicenseManagerCachedLicenseDictionary = nil; NSError *deleteError = nil; if ([RZFileManager() removeItemAtURL:licenseFilePath error:&deleteError] == NO) { LogToConsoleError("Failed to delete user license file with error: %{public}@", deleteError.localizedDescription); return TLOLicenseManagerActionResultCannotWrite; } } return TLOLicenseManagerActionResultSuccess; } BOOL TLOLicenseManagerLicenseFileExists(void) { NSURL *licenseFilePath = TLOLicenseManagerLicenseFilePath(); if (licenseFilePath == nil) { return NO; } BOOL isDirectory = NO; BOOL fileExists = [RZFileManager() fileExistsAtPath:licenseFilePath.path isDirectory:&isDirectory]; return (fileExists && isDirectory == NO); } NSData * _Nullable TLOLicenseManagerLicenseFileContents(void) { NSURL *licenseFilePath = TLOLicenseManagerLicenseFilePath(); if (licenseFilePath == nil) { LogToConsoleError("Unable to determine the path to retrieve license information from"); return nil; } if (TLOLicenseManagerLicenseFileExists() == NO) { return nil; } NSError *readError = nil; NSData *licenseContents = [NSData dataWithContentsOfURL:licenseFilePath options:0 error:&readError]; if (licenseContents == nil) { LogToConsoleError("Unable to read user license file. Error: %{public}@", readError.localizedDescription); return nil; } return licenseContents; } TLOLicenseManagerActionResult TLOLicenseManagerLoadLicenseDictionary(void) { NSData *licenseContents = TLOLicenseManagerLicenseFileContents(); if (licenseContents == nil) { return NO; } return TLOLicenseManagerLoadLicenseDictionaryWithData(licenseContents); } TLOLicenseManagerActionResult TLOLicenseManagerLoadLicenseDictionaryWithData(NSData *licenseContents) { NSCParameterAssert(licenseContents != nil); NSDictionary *licenseDictionary = TLOLicenseManagerLicenseDictionaryWithData(licenseContents); if (licenseDictionary == nil) { return TLOLicenseManagerActionResultCannotRead; } TLOLicenseManagerActionResult result = TLOLicenseManagerVerifyLicenseSignatureWithDictionary(licenseDictionary); if (result == TLOLicenseManagerActionResultSuccess) { TLOLicenseManagerCachedLicenseDictionary = [licenseDictionary copy]; } return result; } NSDictionary *TLOLicenseManagerLicenseDictionaryWithData(NSData *licenseContents) { NSCParameterAssert(licenseContents != nil); /* The contents of the user license is /supposed/ to be a properly formatted property list as sent from the license system hosted on www.codeux.com */ NSError *readError = nil; id licenseDictionary = [NSPropertyListSerialization propertyListWithData:licenseContents options:NSPropertyListImmutable format:NULL error:&readError]; if (licenseDictionary == nil || [licenseDictionary isKindOfClass:[NSDictionary class]] == NO) { if (readError) { LogToConsoleError("Failed to convert contents of user license into dictionary. Error: %{public}@", readError.localizedDescription); } return nil; } return licenseDictionary; } NSDictionary * _Nullable TLOLicenseManagerLicenseDictionary(void) { return TLOLicenseManagerCachedLicenseDictionary; } #pragma mark - #pragma mark Public Key Access NSData * _Nullable TLOLicenseManagerPublicKeyContents(void) { /* Find where public key is */ NSURL *publicKeyPath = [RZMainBundle() URLForResource:@"RemoteLicenseSystemPublicKey" withExtension:@"pub"]; if (publicKeyPath == nil) { LogToConsoleError("Unable to find the public key used for verifying signatures"); return nil; } /* Load contents of the public key */ NSError *readError = nil; NSData *publicKeyContents = [NSData dataWithContentsOfURL:publicKeyPath options:0 error:&readError]; if (publicKeyContents == nil) { LogToConsoleError("Unable to read contents of the public key used for verifying signatures. Error: %{public}@", readError.localizedDescription); return nil; } return publicKeyContents; } void TLOLicenseManagerSetPublicKeyIsGenuine(void) { NSData *publicKeyContents = TLOLicenseManagerPublicKeyContents(); if (publicKeyContents == nil) { return; } NSString *publicKeyHash = publicKeyContents.sha256; if ([TLOLicenseManagerHashOfGenuinePublicKey isEqualToString:publicKeyHash]) { TLOLicenseManagerPublicKeyIsGenuineResult = YES; } else { TLOLicenseManagerPublicKeyIsGenuineResult = NO; } } void TLOLicenseManagerPopulatePublicKeyRef(void) { NSData *publicKeyContents = TLOLicenseManagerPublicKeyContents(); if (publicKeyContents == nil) { return; } SecItemImportExportKeyParameters importParameters; importParameters.flags = kSecKeyNoAccessControl; importParameters.version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION; importParameters.accessRef = NULL; importParameters.alertPrompt = NULL; importParameters.alertTitle = NULL; importParameters.passphrase = NULL; importParameters.keyAttributes = NULL; importParameters.keyUsage = NULL; SecExternalItemType itemType = kSecItemTypePublicKey; SecExternalFormat externalFormat = kSecFormatPEMSequence; int flags = 0; CFArrayRef tempArray = NULL; OSStatus operationStatus = SecItemImport((__bridge CFDataRef)(publicKeyContents), NULL, &externalFormat, &itemType, flags, &importParameters, NULL, &tempArray); if (operationStatus == noErr) { TLOLicenseManagerPublicKey = (SecKeyRef)CFArrayGetValueAtIndex(tempArray, 0); CFRetain(TLOLicenseManagerPublicKey); CFRelease(tempArray); return; } LogToConsoleError("SecItemImport() failed to import public key with status code: %{public}i", operationStatus); } #pragma mark - #pragma mark User License File Information NSString * _Nullable TLOLicenseManagerLicenseOwnerName(void) { NSDictionary *licenseDictionary = TLOLicenseManagerLicenseDictionary(); if (licenseDictionary == nil) { return nil; } return licenseDictionary[TLOLicenseManagerLicenseDictionaryKeyOwnerName]; } NSString * _Nullable TLOLicenseManagerLicenseOwnerContactAddress(void) { NSDictionary *licenseDictionary = TLOLicenseManagerLicenseDictionary(); if (licenseDictionary == nil) { return nil; } return licenseDictionary[TLOLicenseManagerLicenseDictionaryKeyOwnerContactAddress]; } NSString * _Nullable TLOLicenseManagerLicenseKey(void) { NSDictionary *licenseDictionary = TLOLicenseManagerLicenseDictionary(); if (licenseDictionary == nil) { return nil; } return licenseDictionary[TLOLicenseManagerLicenseDictionaryKeyLicenseKey]; } NSUInteger TLOLicenseManagerLicenseGeneration(void) { NSDictionary *licenseDictionary = TLOLicenseManagerLicenseDictionary(); if (licenseDictionary == nil) { return 0; } return [licenseDictionary unsignedIntegerForKey:TLOLicenseManagerLicenseDictionaryKeyGeneration]; } NSString * _Nullable TLOLicenseManagerLicenseCreationDate(void) { NSDictionary *licenseDictionary = TLOLicenseManagerLicenseDictionary(); if (licenseDictionary == nil) { return nil; } return licenseDictionary[TLOLicenseManagerLicenseDictionaryKeyCreationDate]; } NSString * _Nullable TLOLicenseManagerLicenseCreationDateFormatted(void) { NSString *creationDateString = TLOLicenseManagerLicenseCreationDate(); if (creationDateString == nil) { return nil; } NSDateFormatter *dateFormatter = [NSDateFormatter new]; dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss"; NSDate *creationDate = [dateFormatter dateFromString:creationDateString]; if (creationDate == nil) { return creationDateString; } dateFormatter.dateFormat = nil; dateFormatter.doesRelativeDateFormatting = YES; dateFormatter.dateStyle = NSDateFormatterLongStyle; dateFormatter.timeStyle = NSDateFormatterNoStyle; return [dateFormatter stringFromDate:creationDate]; } #pragma mark - #pragma mark Authorization NSString * TLOLicenseManagerAuthorizationCode(void) { NSString *code = [[NSUserDefaults standardUserDefaults] stringForKey:@"TLOLicenseManagerAuthorizationCode"]; if (code == nil) { code = @""; } return code; } #pragma mark - #pragma mark Blacklist void TLOLicenseManagerDeleteLicenseFileIfBlacklisted(void) { NSString *licenseKey = TLOLicenseManagerLicenseKey(); if (licenseKey == nil) { return; } if (TLOLicenseManagerLicenseKeyBlacklisted(licenseKey) == NO) { return; } LogToConsoleInfo("License key '%{private}@' is blacklisted", licenseKey); TLOLicenseManagerDeleteLicenseFile(); } BOOL TLOLicenseManagerLicenseKeyBlacklisted(NSString *licenseKey) { NSCParameterAssert(licenseKey != nil); NSArray *blacklistedLicenseKeys = nil; if (blacklistedLicenseKeys == nil) { blacklistedLicenseKeys = @[ @"mushy-argyle-oryx-428112186934176870777339608", @"wicked-plaid-weasel-0681043544455415517323623", @"juicy-cyan-toad-72508217265703758416002094229", @"cheerful-turquoise-zebra-81864329409734131515", @"many-wavy-fly-7043700524134077492967431288170", @"helpful-jade-quelea-7403506199409693779877124", @"obnoxious-onyx-bear-9755065646001620521415702", ]; } return [blacklistedLicenseKeys containsObject:licenseKey]; } #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/License Manager/Standalone/TLOLicenseManagerDownloader.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSStringHelper.h" #import "TXMasterController.h" #import "TXMenuController.h" #import "TDCAlert.h" #import "TPCApplicationInfo.h" #import "TLOLocalization.h" #import "TLOLicenseManagerPrivate.h" #import "TLOLicenseManagerDownloaderPrivate.h" NS_ASSUME_NONNULL_BEGIN /* URLs for performing certain actions with license keys. */ NSString * const TLOLicenseManagerDownloaderLicenseAPIActivationURL = @"https://licensing.textualapp.com/activateLicense.cs"; NSString * const TLOLicenseManagerDownloaderLicenseAPISendLostLicenseURL = @"https://licensing.textualapp.com/sendLostLicense.cs"; NSString * const TLOLicenseManagerDownloaderLicenseAPIMigrateAppStoreURL = @"https://licensing.textualapp.com/convertReceiptToLicense.cs"; NSString * const TLOLicenseManagerDownloaderLicenseAPILicenseUpgradeEligibilityURL = @"https://licensing.textualapp.com/upgradeEligibility.cs"; NSString * const TLOLicenseManagerDownloaderLicenseAPIReceiptUpgradeEligibilityURL = @"https://licensing.textualapp.com/upgradeEligibilityForReceipt.cs"; /* The license API throttles requests to prevent abuse. The following HTTP status code will inform Textual if it the license API has been overwhelmed. */ NSUInteger const TLOLicenseManagerDownloaderRequestHTTPStatusSuccess = 200; // OK NSUInteger const TLOLicenseManagerDownloaderRequestHTTPStatusServiceIsBusy = 429; // Service is Busy NSUInteger const TLOLicenseManagerDownloaderRequestHTTPStatusTryAgainLater = 503; // Service Unavailable /* The following constants note status codes that may be returned part of the contents of a license API response body. This is not a complete list. */ NSUInteger const TLOLicenseManagerDownloaderRequestStatusCodeSuccess = 0; NSUInteger const TLOLicenseManagerDownloaderRequestStatusCodeGenericError = 2000000; NSUInteger const TLOLicenseManagerDownloaderRequestStatusCodeServiceIsBusy = 2000001; /* Private header */ typedef void (^TLOLicenseManagerDownloaderConnectionCompletionBlock)(TLOLicenseManagerDownloaderRequestType requestType, NSURLResponse * _Nullable response, NSData * _Nullable data); @interface TLOLicenseManagerDownloaderConnection : NSObject @property (nonatomic, weak) TLOLicenseManagerDownloader *delegate; // To be set by caller @property (nonatomic, assign) TLOLicenseManagerDownloaderRequestType requestType; // To be set by caller @property (nonatomic, copy) NSDictionary *requestContextInfo; // Information set by caller such as license key or e-mail address @property (nonatomic, strong) NSURLSessionTask *sessionTask; - (void)performRequest:(TLOLicenseManagerDownloaderConnectionCompletionBlock)completionBlock; - (void)cancelRequest; @end @interface TLOLicenseManagerDownloader () @property (nonatomic, strong, nullable) TLOLicenseManagerDownloaderConnection *activeConnection; @end #define _connectionTimeoutInterval 30.0 @implementation TLOLicenseManagerDownloader #pragma mark - #pragma mark Public Interface - (void)activateLicense:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); NSDictionary *contextInfo = @{@"licenseKey" : licenseKey}; [self performRequestType:TLOLicenseManagerDownloaderRequestTypeActivation context:contextInfo]; } - (void)deactivateLicense { BOOL operationResult = self.actionBlock(TLOLicenseManagerDownloaderRequestStatusCodeSuccess, nil); if (self.completionBlock) { self.completionBlock(operationResult, TLOLicenseManagerDownloaderRequestStatusCodeSuccess, nil); } } - (void)checkUpgradeEligibilityOfLicense:(NSString *)licenseKey { NSParameterAssert(licenseKey != nil); NSDictionary *contextInfo = @{@"licenseKey" : licenseKey}; [self performRequestType:TLOLicenseManagerDownloaderRequestTypeLicenseUpgradeEligibility context:contextInfo]; } - (void)checkUpgradeEligibilityOfReceipt:(NSString *)receiptData { NSParameterAssert(receiptData != nil); NSString *macAddress = [XRSystemInformation formattedEthernetMacAddress]; NSParameterAssert(macAddress != nil); NSDictionary *contextInfo = @{ @"receiptData" : receiptData, @"licenseOwnerMacAddress" : macAddress }; [self performRequestType:TLOLicenseManagerDownloaderRequestTypeReceiptUpgradeEligibility context:contextInfo]; } - (void)requestLostLicenseKeyForContactAddress:(NSString *)contactAddress { NSParameterAssert(contactAddress != nil); NSDictionary *contextInfo = @{@"licenseOwnerContactAddress" : contactAddress}; [self performRequestType:TLOLicenseManagerDownloaderRequestTypeSendLostLicense context:contextInfo]; } - (void)migrateMacAppStorePurchase:(NSString *)receiptData licenseOwnerName:(NSString *)licenseOwnerName licenseOwnerContactAddress:(NSString *)licenseOwnerContactAddress { NSParameterAssert(receiptData != nil); NSParameterAssert(licenseOwnerName != nil); NSParameterAssert(licenseOwnerContactAddress != nil); NSString *macAddress = [XRSystemInformation formattedEthernetMacAddress]; NSParameterAssert(macAddress != nil); NSDictionary *contextInfo = @{ @"receiptData" : receiptData, @"licenseOwnerName" : licenseOwnerName, @"licenseOwnerContactAddress" : licenseOwnerContactAddress, @"licenseOwnerMacAddress" : macAddress }; [self performRequestType:TLOLicenseManagerDownloaderRequestTypeMigrateAppStore context:contextInfo]; } - (void)performRequestType:(TLOLicenseManagerDownloaderRequestType)requestType context:(NSDictionary *)requestContext { NSParameterAssert(requestContext != nil); if ( self.activeConnection != nil) { [self.activeConnection cancelRequest]; } TLOLicenseManagerDownloaderConnection *connectionObject = [TLOLicenseManagerDownloaderConnection new]; connectionObject.delegate = self; connectionObject.requestContextInfo = requestContext; connectionObject.requestType = requestType; __weak TLOLicenseManagerDownloader *weakSelf = self; [connectionObject performRequest:^(TLOLicenseManagerDownloaderRequestType requestType, NSURLResponse *response, NSData *data) { /* Connection uses NSURLSession which is a background task. */ /* The result of processing the response will update the UI which must always take place on the main thread. */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [weakSelf processResponse:response forRequestType:requestType contents:data]; }); weakSelf.activeConnection = nil; }]; self.activeConnection = connectionObject; } - (void)cancelRequest { if (self.activeConnection == nil) { return; } [self.activeConnection cancelRequest]; self.activeConnection = nil; self.actionBlock = nil; self.errorBlock = nil; self.completionBlock = nil; } - (void)processResponse:(nullable NSURLResponse *)response forRequestType:(TLOLicenseManagerDownloaderRequestType)requestType contents:(nullable NSData *)responseContents { /* We assume the response failed until proven otherwise. */ NSUInteger responseStatusCode = TLOLicenseManagerDownloaderRequestHTTPStatusTryAgainLater; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { responseStatusCode = ((NSHTTPURLResponse *)response).statusCode; } [self processResponseForRequestType:requestType httpStatusCode:responseStatusCode contents:responseContents]; } - (void)processResponseForRequestType:(TLOLicenseManagerDownloaderRequestType)requestType httpStatusCode:(NSUInteger)responseStatusCode contents:(nullable NSData *)responseContents { /* The license API returns content as property lists, including errors. This method will try to convert the returned contents into an NSDictionary (assuming its a valid property list). If that fails, then the method shows generic failure reason and logs to the console that the contents could not parsed. */ /* Define defaults */ id propertyList = nil; __block NSUInteger apiStatusCode = TLOLicenseManagerDownloaderRequestStatusCodeGenericError; if (responseStatusCode == TLOLicenseManagerDownloaderRequestHTTPStatusServiceIsBusy) { apiStatusCode = TLOLicenseManagerDownloaderRequestStatusCodeServiceIsBusy; } __block id apiStatusContext = nil; /* Helper blocks */ void (^performCompletionBlock)(BOOL) = ^(BOOL success) { if (self.completionBlock) { self.completionBlock(success, apiStatusCode, apiStatusContext); } }; void (^presentError)(void) = ^(void) { if ( /* Perform error block if it was not performed by some other condition presented below. */ (self.errorBlock == nil || self.errorBlock(apiStatusCode, apiStatusContext) == NO) && /* Do we even present an error? */ self.isSilentOnFailure == NO) { [self presentTryAgainLaterErrorDialog]; } performCompletionBlock(NO); }; void (^presentErrorUnconditionally)(void) = ^(void) { [self presentTryAgainLaterErrorDialog]; performCompletionBlock(NO); }; /* Convert contents into a dictionary */ if (responseContents) { NSError *propertyListReadError = nil; propertyList = [NSPropertyListSerialization propertyListWithData:responseContents options:NSPropertyListImmutable format:NULL error:&propertyListReadError]; if (propertyList == nil || [propertyList isKindOfClass:[NSDictionary class]] == NO) { if (propertyListReadError) { LogToConsoleError("Failed to convert contents of request into dictionary. Error: %{public}@", propertyListReadError.localizedDescription); } } } id l_statusCode = propertyList[@"Status Code"]; if (l_statusCode == nil || [l_statusCode isKindOfClass:[NSNumber class]] == NO) { LogToConsoleError("'Status Code' is nil or not of kind 'NSNumber'"); return presentError(); } apiStatusCode = [l_statusCode unsignedIntegerValue]; apiStatusContext = propertyList[@"Status Context"]; /* Process contents */ if (responseStatusCode == TLOLicenseManagerDownloaderRequestHTTPStatusSuccess) { /* Process successful results */ if (requestType == TLOLicenseManagerDownloaderRequestTypeActivation && apiStatusCode == TLOLicenseManagerDownloaderRequestStatusCodeSuccess) { if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSData class]] == NO) { LogToConsoleError("'Status Context' is nil or not of kind 'NSData'"); return presentError(); } if (self.actionBlock != nil && self.actionBlock(apiStatusCode, apiStatusContext) == NO) { LogToConsoleError("Failed to write user license file contents"); return presentError(); } if (self.isSilentOnSuccess == NO) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[k39-7l]") title:TXTLS(@"TLOLicenseManager[jbs-64]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } return performCompletionBlock(YES); } else if (requestType == TLOLicenseManagerDownloaderRequestTypeSendLostLicense && apiStatusCode == TLOLicenseManagerDownloaderRequestStatusCodeSuccess) { if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("'Status Context' is nil or not of kind 'NSDictionary'"); return presentError(); } if (self.actionBlock != nil && self.actionBlock(apiStatusCode, apiStatusContext) == NO) { LogToConsoleError("Action blocked returned error"); return presentError(); } NSString *licenseOwnerContactAddress = apiStatusContext[@"licenseOwnerContactAddress"]; if (licenseOwnerContactAddress.length == 0) { LogToConsoleError("'licenseOwnerContactAddress' is nil or of zero length"); return presentError(); } if (self.isSilentOnSuccess == NO) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[fxj-s6]", licenseOwnerContactAddress) title:TXTLS(@"TLOLicenseManager[m4q-ul]", licenseOwnerContactAddress) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } return performCompletionBlock(YES); } else if (requestType == TLOLicenseManagerDownloaderRequestTypeMigrateAppStore && apiStatusCode == TLOLicenseManagerDownloaderRequestStatusCodeSuccess) { if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("'Status Context' is nil or not of kind 'NSDictionary'"); return presentError(); } if (self.actionBlock != nil && self.actionBlock(apiStatusCode, apiStatusContext) == NO) { LogToConsoleError("Action blocked returned error"); return presentError(); } NSString *licenseOwnerContactAddress = apiStatusContext[@"licenseOwnerContactAddress"]; if (licenseOwnerContactAddress.length == 0) { LogToConsoleError("'licenseOwnerContactAddress' is nil or of zero length"); return presentError(); } if (self.isSilentOnSuccess == NO) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[yxk-ej]", licenseOwnerContactAddress) title:TXTLS(@"TLOLicenseManager[vxq-oa]", licenseOwnerContactAddress) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } return performCompletionBlock(YES); } else if (requestType == TLOLicenseManagerDownloaderRequestTypeLicenseUpgradeEligibility && apiStatusCode == TLOLicenseManagerDownloaderRequestStatusCodeSuccess) { if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("'Status Context' is nil or not of kind 'NSDictionary'"); return presentError(); } if (self.actionBlock != nil) { (void)self.actionBlock(apiStatusCode, apiStatusContext); } return performCompletionBlock(YES); } else if (requestType == TLOLicenseManagerDownloaderRequestTypeReceiptUpgradeEligibility && apiStatusCode == TLOLicenseManagerDownloaderRequestStatusCodeSuccess) { if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("'Status Context' is nil or not of kind 'NSDictionary'"); return presentError(); } if (self.actionBlock != nil) { (void)self.actionBlock(apiStatusCode, apiStatusContext); } return performCompletionBlock(YES); } } else // TLOLicenseManagerDownloaderRequestStatusCodeSuccess { if ((self.errorBlock != nil && self.errorBlock(apiStatusCode, apiStatusContext)) || self.isSilentOnFailure) { if (self.completionBlock) { self.completionBlock(NO, apiStatusCode, apiStatusContext); } return; } /* Errors related to license activation. */ BOOL presentError = NO; if (apiStatusCode == TLOLicenseManagerDownloaderRequestStatusCodeServiceIsBusy) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[fvx-90]") title:TXTLS(@"TLOLicenseManager[13u-4p]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } else if (requestType == TLOLicenseManagerDownloaderRequestTypeActivation && apiStatusCode == 6500000) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[wc7-mn]") title:TXTLS(@"TLOLicenseManager[fg6-gf]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } else if (requestType == TLOLicenseManagerDownloaderRequestTypeActivation && apiStatusCode == 6500001) { if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("'Status Context' kind is not of 'NSDictionary'"); return presentErrorUnconditionally(); } NSString *licenseKey = apiStatusContext[@"licenseKey"]; if (licenseKey.length == 0) { LogToConsoleError("'licenseKey' is nil or of zero length"); return presentErrorUnconditionally(); } BOOL userResponse = [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[w1n-n0]") title:TXTLS(@"TLOLicenseManager[u8h-qv]", licenseKey.prettyLicenseKey) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:TXTLS(@"TLOLicenseManager[vgp-j6]")]; if (userResponse == NO) { // NO = alternate button [self contactSupport]; } } else if (requestType == TLOLicenseManagerDownloaderRequestTypeActivation && apiStatusCode == 6500002) { if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("'Status Context' kind is not of 'NSDictionary'"); return presentErrorUnconditionally(); } NSString *licenseKey = apiStatusContext[@"licenseKey"]; if (licenseKey.length == 0) { LogToConsoleError("'licenseKey' is nil or of zero length"); return presentErrorUnconditionally(); } NSInteger licenseKeyActivationLimit = [apiStatusContext integerForKey:@"licenseKeyActivationLimit"]; [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[6aa-ow]", licenseKeyActivationLimit) title:TXTLS(@"TLOLicenseManager[o66-ox]", licenseKey.prettyLicenseKey) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } /* Errors related to lost license recovery. */ else if (requestType == TLOLicenseManagerDownloaderRequestTypeSendLostLicense && apiStatusCode == 6400000) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[dio-y9]") title:TXTLS(@"TLOLicenseManager[ocm-03]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } else if (requestType == TLOLicenseManagerDownloaderRequestTypeSendLostLicense && apiStatusCode == 6400001) { if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("'Status Context' kind is not of 'NSDictionary'"); return presentErrorUnconditionally(); } NSString *originalInput = apiStatusContext[@"originalInput"]; if (originalInput.length == 0) { LogToConsoleError("'originalInput' is nil or of zero length"); return presentErrorUnconditionally(); } [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[6zh-jr]", originalInput) title:TXTLS(@"TLOLicenseManager[r87-jw]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } /* Error messages related to Mac App Store migration. */ else if (requestType == TLOLicenseManagerDownloaderRequestTypeMigrateAppStore && apiStatusCode == 6600002) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[bu4-zk]") title:TXTLS(@"TLOLicenseManager[ztd-5y]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } else if (requestType == TLOLicenseManagerDownloaderRequestTypeMigrateAppStore && apiStatusCode == 6600003) { /* We do not present a custom dialog for this error, but we still log the contents of the context to the console to help diagnose issues. */ if (apiStatusContext == nil || [apiStatusContext isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("'Status Context' kind is not of 'NSDictionary'"); return presentErrorUnconditionally(); } NSString *errorMessage = apiStatusContext[@"Error Message"]; if (errorMessage.length == 0) { LogToConsoleError("'errorMessage' is nil or of zero length"); return presentErrorUnconditionally(); } LogToConsoleError("Receipt validation failed:\n%{public}@", errorMessage); [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[ujo-cd]", errorMessage) title:TXTLS(@"TLOLicenseManager[p9s-ak]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } else if (requestType == TLOLicenseManagerDownloaderRequestTypeMigrateAppStore && apiStatusCode == 6600004) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[36y-49]") title:TXTLS(@"TLOLicenseManager[enb-hw]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } else if (requestType == TLOLicenseManagerDownloaderRequestTypeMigrateAppStore && apiStatusCode == 6600006) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[do9-8x]") title:TXTLS(@"TLOLicenseManager[f49-rk]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } else if (requestType == TLOLicenseManagerDownloaderRequestTypeMigrateAppStore && apiStatusCode == 6600007) { [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[4n2-ps]") title:TXTLS(@"TLOLicenseManager[t28-j9]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } else { presentError = YES; } /* presentError defaults to NO because all conditions are caught above except those that may be added in future versions of the API not yet supported by this code. */ if (presentError) { presentErrorUnconditionally(); } else { performCompletionBlock(NO); } } // TLOLicenseManagerDownloaderRequestStatusCodeSuccess } - (void)presentTryAgainLaterErrorDialog { BOOL userResponse = [TDCAlert modalAlertWithMessage:TXTLS(@"TLOLicenseManager[1cv-0v]") title:TXTLS(@"TLOLicenseManager[nhh-ts]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:TXTLS(@"TLOLicenseManager[bqw-cv]")]; if (userResponse == NO) { // NO = alternate button [self contactSupport]; } } - (void)contactSupport { [menuController() contactSupport:nil]; } @end #pragma mark - #pragma mark Connection Assistant @implementation TLOLicenseManagerDownloaderConnection - (NSURL *)requestURL { NSString *requestURLString = nil; if (self.requestType == TLOLicenseManagerDownloaderRequestTypeActivation) { requestURLString = TLOLicenseManagerDownloaderLicenseAPIActivationURL; } else if (self.requestType == TLOLicenseManagerDownloaderRequestTypeSendLostLicense) { requestURLString = TLOLicenseManagerDownloaderLicenseAPISendLostLicenseURL; } else if (self.requestType == TLOLicenseManagerDownloaderRequestTypeMigrateAppStore) { requestURLString = TLOLicenseManagerDownloaderLicenseAPIMigrateAppStoreURL; } else if (self.requestType == TLOLicenseManagerDownloaderRequestTypeLicenseUpgradeEligibility) { requestURLString = TLOLicenseManagerDownloaderLicenseAPILicenseUpgradeEligibilityURL; } else if (self.requestType == TLOLicenseManagerDownloaderRequestTypeReceiptUpgradeEligibility) { requestURLString = TLOLicenseManagerDownloaderLicenseAPIReceiptUpgradeEligibilityURL; } return [NSURL URLWithString:requestURLString]; } - (NSString *)encodedRequestContextValue:(NSString *)contextKey { NSParameterAssert(contextKey != nil); NSString *contextValue = self.requestContextInfo[contextKey]; return contextValue.percentEncodedString; } - (BOOL)populateRequestPostData:(NSMutableURLRequest *)connectionRequest { NSParameterAssert(connectionRequest != nil); /* Post parameter(s) defined by this method are subject to change at any time because obviously, the license API is not public interface */ /* Post data is sent as form values with key/value pairs. */ NSString *currentUserLanguage = [NSLocale currentLocale].localeIdentifier; NSString *applicationVersion = [TPCApplicationInfo applicationVersion].percentEncodedString; NSString *authorization = TLOLicenseManagerAuthorizationCode(); NSString *requestBodyString = nil; if (self.requestType == TLOLicenseManagerDownloaderRequestTypeActivation) { NSString *encodedContextInfo = [self encodedRequestContextValue:@"licenseKey"]; requestBodyString = [NSString stringWithFormat:@"licenseKey=%@&lang=%@&version=%@&authorization=%@", encodedContextInfo, currentUserLanguage, applicationVersion, authorization]; } else if (self.requestType == TLOLicenseManagerDownloaderRequestTypeSendLostLicense) { NSString *encodedContextInfo = [self encodedRequestContextValue:@"licenseOwnerContactAddress"]; requestBodyString = [NSString stringWithFormat:@"licenseOwnerContactAddress=%@&lang=%@&version=%@&authorization=%@", encodedContextInfo, currentUserLanguage, applicationVersion, authorization]; } else if (self.requestType == TLOLicenseManagerDownloaderRequestTypeMigrateAppStore) { NSString *receiptData = [self encodedRequestContextValue:@"receiptData"]; NSString *licenseOwnerName = [self encodedRequestContextValue:@"licenseOwnerName"]; NSString *licenseOwnerContactAddress = [self encodedRequestContextValue:@"licenseOwnerContactAddress"]; NSString *licenseOwnerMacAddress = [self encodedRequestContextValue:@"licenseOwnerMacAddress"]; requestBodyString = [NSString stringWithFormat:@"receiptData=%@&licenseOwnerMacAddress=%@&licenseOwnerContactAddress=%@&licenseOwnerName=%@&lang=%@&version=%@&authorization=%@", receiptData, licenseOwnerMacAddress, licenseOwnerContactAddress, licenseOwnerName, currentUserLanguage, applicationVersion, authorization]; } else if (self.requestType == TLOLicenseManagerDownloaderRequestTypeLicenseUpgradeEligibility) { NSString *encodedContextInfo = [self encodedRequestContextValue:@"licenseKey"]; requestBodyString = [NSString stringWithFormat:@"licenseKey=%@&lang=%@&version=%@&authorization=%@", encodedContextInfo, currentUserLanguage, applicationVersion, authorization]; } else if (self.requestType == TLOLicenseManagerDownloaderRequestTypeReceiptUpgradeEligibility) { NSString *receiptData = [self encodedRequestContextValue:@"receiptData"]; NSString *licenseOwnerMacAddress = [self encodedRequestContextValue:@"licenseOwnerMacAddress"]; requestBodyString = [NSString stringWithFormat:@"receiptData=%@&licenseOwnerMacAddress=%@&lang=%@&version=%@&authorization=%@", receiptData, licenseOwnerMacAddress, currentUserLanguage, applicationVersion, authorization]; } if (requestBodyString == nil) { return NO; } NSData *requestBodyData = [requestBodyString dataUsingEncoding:NSASCIIStringEncoding]; connectionRequest.HTTPMethod = @"POST"; [connectionRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; connectionRequest.HTTPBody = requestBodyData; return YES; } - (void)dealloc { self.delegate = nil; } - (void)cancelRequest { if ( self.sessionTask) { [self.sessionTask cancel]; } } - (void)performRequest:(TLOLicenseManagerDownloaderConnectionCompletionBlock)completionBlock { /* Setup request including HTTP POST data. Return NO on failure */ NSURL *requestURL = [self requestURL]; if (requestURL == nil) { return; } NSMutableURLRequest *baseRequest = [NSMutableURLRequest requestWithURL:requestURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:_connectionTimeoutInterval]; if ([self populateRequestPostData:baseRequest] == NO) { return; } /* Create the connection and start it */ TLOLicenseManagerDownloaderRequestType requestType = self.requestType; __weak TLOLicenseManagerDownloaderConnection *weakSelf = self; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionTask *task = [session dataTaskWithRequest:baseRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (data == nil) { if (error) { LogToConsoleError("Request failed with error: %{public}@", error.localizedDescription); } completionBlock(requestType, nil, nil); return; } completionBlock(requestType, response, data); weakSelf.sessionTask = nil; }]; [task resume]; self.sessionTask = task; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/License Manager/Standalone/TLOLicenseManagerLastGen.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPathInfo.h" #import "TLOLicenseManagerPrivate.h" #import "TLOLicenseManagerLastGenPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 @implementation TLOLicenseManagerLastGen #pragma mark - #pragma mark Public Interface + (nullable NSString *)licenseKey { NSDictionary *licenseDictionary = [self lastGenLicenseDictionary]; if (licenseDictionary == nil) { return nil; } return [self lastGenLicenseKeyWithLicenseDictionary:licenseDictionary]; } + (nullable NSString *)licenseKeyForLicenseContents:(NSData *)licenseContents { NSParameterAssert(licenseContents != nil); NSDictionary *licenseDictionary = [self lastGenLicenseDictionaryWithData:licenseContents]; if (licenseDictionary == nil) { return nil; } NSString *licenseKey = [self lastGenLicenseKeyWithLicenseDictionary:licenseDictionary]; return licenseKey; } #pragma mark - #pragma mark Public Interface + (nullable NSString *)lastGenLicenseKeyWithLicenseDictionary:(NSDictionary *)licenseDictionary { NSParameterAssert(licenseDictionary != nil); /* The integrity of the file and whether it is properly signed does not matter for this class. We are only interested in whether the dictionary we have is last gen and whether it has a valid license key. If both are true, then we can advise user that they need to upgrade. Nothing more. */ NSString *licenseKey = licenseDictionary[@"licenseKey"]; if (TLOLicenseManagerLicenseKeyIsValid(licenseKey) == NO) { return nil; } /* Last gen license dictionary did not have a license generation value. */ /* If that is not present, than consider the dictionary last gen. */ id licenseGeneration = licenseDictionary[@"licenseGeneration"]; if (licenseGeneration == nil || [licenseGeneration isKindOfClass:[NSNumber class]] == NO) { return licenseKey; } if ([licenseGeneration unsignedIntegerValue] != TLOLicenseManagerCurrentLicenseGeneration) { return licenseKey; } return nil; } + (nullable NSURL *)lastGenLicenseFilePath { NSURL *sourceURL = [TPCPathInfo applicationSupportURL]; if (sourceURL == nil) { return nil; } NSURL *baseURL = [sourceURL URLByAppendingPathComponent:@"/Textual_User_License.plist"]; return baseURL; } + (nullable NSData *)lastGenLicenseFileContents { NSURL *licenseFilePath = [self lastGenLicenseFilePath]; if (licenseFilePath == nil) { LogToConsoleError("Unable to determine the path to retrieve license information from"); return nil; } if ([RZFileManager() fileExistsAtURL:licenseFilePath] == NO) { return nil; } NSError *readError = nil; NSData *licenseContents = [NSData dataWithContentsOfURL:licenseFilePath options:0 error:&readError]; if (licenseContents == nil) { LogToConsoleError("Unable to read user license file (%{public}@). Error: %{public}@", licenseFilePath.standardizedTildePath, readError.localizedDescription); return nil; } return licenseContents; } + (nullable NSDictionary *)lastGenLicenseDictionary { NSData *licenseContents = [self lastGenLicenseFileContents]; if (licenseContents == nil) { return nil; } return [self lastGenLicenseDictionaryWithData:licenseContents]; } + (nullable NSDictionary *)lastGenLicenseDictionaryWithData:(NSData *)licenseContents { NSParameterAssert(licenseContents != nil); /* The contents of the user license is /supposed/ to be a properly formatted property list as sent from the license system hosted on www.codeux.com */ NSError *readError = nil; id licenseDictionary = [NSPropertyListSerialization propertyListWithData:licenseContents options:NSPropertyListImmutable format:NULL error:&readError]; if (licenseDictionary == nil || [licenseDictionary isKindOfClass:[NSDictionary class]] == NO) { if (readError) { LogToConsoleError("Failed to convert contents of user license into dictionary. Error: %{public}@", readError.localizedDescription); } return nil; } return licenseDictionary; } @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOEncryptionManager.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" #import "TXMenuController.h" #import "TDCAlert.h" #import "TLOLocalization.h" #import "TVCMainWindow.h" #import "TVCMainWindowTitlebarAccessoryViewPrivate.h" #import "TVCLogRenderer.h" #import "TPCPathInfoPrivate.h" #import "TPCPreferencesLocal.h" #import "IRCClientPrivate.h" #import "IRCChannelPrivate.h" #import "IRCWorld.h" #import "TLOEncryptionManagerPrivate.h" NS_ASSUME_NONNULL_BEGIN #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 @interface TLOEncryptionManager () @property (nonatomic, strong, nullable) OTRKitFingerprintManagerDialog *fingerprintManagerDialog; @end @interface TLOEncryptionManagerEncodingDecodingObject : NSObject // Properties that should be manipulated to provide context information @property (nonatomic, copy, nullable) TLOEncryptionManagerEncodingDecodingCallbackBlock encodingCallback; @property (nonatomic, copy, nullable) TLOEncryptionManagerInjectCallbackBlock injectionCallback; @property (nonatomic, copy) NSString *messageFrom; @property (nonatomic, copy) NSString *messageTo; @property (nonatomic, copy) NSString *messageBody; // unencrypted value @end @implementation TLOEncryptionManager #pragma mark - #pragma mark Initialization - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [self setupEncryptionManager]; } - (nullable NSString *)pathToStoreEncryptionSecrets { NSString *sourcePath = [TPCPathInfo groupContainerApplicationSupport]; if (sourcePath == nil) { return nil; } NSString *basePath = [sourcePath stringByAppendingPathComponent:@"/Encryption Components/"]; [TPCPathInfo _createDirectoryAtPath:basePath]; return basePath; } - (void)setupEncryptionManager { OTRKit *otrKit = [OTRKit sharedInstance]; otrKit.accountNameSeparator = @"@"; otrKit.delegate = (id)self; [otrKit setupWithDataPath:[self pathToStoreEncryptionSecrets]]; [self prepareEncryptionComponentPath:otrKit.fingerprintsPath]; [self prepareEncryptionComponentPath:otrKit.instanceTagsPath]; [self prepareEncryptionComponentPath:otrKit.privateKeyPath]; NSURL *componentPathURL = [NSURL fileURLWithPath:otrKit.dataPath isDirectory:YES]; NSError *attributesChangeError = nil; if ([componentPathURL setResourceValue:@(YES) forKey:NSURLIsHiddenKey error:&attributesChangeError] == NO) { LogToConsoleError("Failed to hide the folder at the path '%{private}@': %{private}@", componentPathURL.standardizedTildePath, attributesChangeError.localizedDescription); } [otrKit setMaximumProtocolSize:[self otrKitProtocolMaximumMessageSize] forProtocol:[self otrKitProtocol]]; [self updatePolicy]; } - (void)prepareEncryptionComponentPath:(NSString *)path { NSParameterAssert(path != nil); /* Create the path if it does not already exist. */ if ([RZFileManager() fileExistsAtPath:path] == NO) { NSError *writeError = nil; if ([@"" writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&writeError] == NO) { LogToConsoleError("Failed to create base file for encryption component at path: %{private}@", writeError.localizedDescription); } } /* Files are stored in a location that is accessible to Time Machine which means we must mark the files to not be backed up. */ NSURL *pathURL = [NSURL fileURLWithPath:path isDirectory:NO]; NSError *attributesChangeError = nil; if ([pathURL setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:&attributesChangeError] == NO) { LogToConsoleError("Failed to exclude the files at the path '%{private}@' from backup: %{private}@", pathURL.standardizedTildePath, attributesChangeError.localizedDescription); } } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Preparing encryption manager"); } #pragma mark - #pragma mark Fingerprint Manager - (void)presentListOfFingerprints { if (self.fingerprintManagerDialog == nil) { OTRKitFingerprintManagerDialog *dialog = [OTRKitFingerprintManagerDialog new]; dialog.delegate = (id)self; self.fingerprintManagerDialog = dialog; } [self.fingerprintManagerDialog open:mainWindow()]; } #pragma mark - #pragma mark Account Name Information - (NSString *)accountNameForUser:(NSString *)nickname onClient:(IRCClient *)client { NSParameterAssert(nickname != nil); NSParameterAssert(client != nil); return [NSString stringWithFormat:@"%@%@%@", nickname, [OTRKit sharedInstance].accountNameSeparator, client.uniqueIdentifier]; } - (nullable NSString *)nicknameFromAccountName:(NSString *)accountName { NSParameterAssert(accountName != nil); NSString *nickname = [[OTRKit sharedInstance] leftPortionOfAccountName:accountName]; return nickname; } - (nullable IRCClient *)connectionFromAccountName:(NSString *)accountName { NSParameterAssert(accountName != nil); NSString *clientIdentifier = [[OTRKit sharedInstance] rightPortionOfAccountName:accountName]; return [worldController() findClientWithId:clientIdentifier]; } #pragma mark - #pragma mark Starting Encryption & Stopping Encryption - (void)beginConversationWith:(NSString *)messageTo from:(NSString *)messageFrom { [self refreshConversationWith:messageTo from:messageFrom presentMessage:TXTLS(@"OffTheRecord[aii-2q]")]; } - (void)endConversationWith:(NSString *)messageTo from:(NSString *)messageFrom { NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; if (currentState == OTRKitMessageStateEncrypted) { [[OTRKit sharedInstance] disableEncryptionWithUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; } else { [self presentErrorMessage:TXTLS(@"OffTheRecord[g9p-8r]") withAccountName:messageTo]; } } - (void)refreshConversationWith:(NSString *)messageTo from:(NSString *)messageFrom { [self refreshConversationWith:messageTo from:messageFrom presentMessage:TXTLS(@"OffTheRecord[f59-3b]")]; } - (void)refreshConversationWith:(NSString *)messageTo from:(NSString *)messageFrom presentMessage:(NSString *)message { NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); NSParameterAssert(message != nil); [self presentMessage:message withAccountName:messageTo]; OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; if (currentState == OTRKitMessageStateEncrypted) { [[OTRKit sharedInstance] disableEncryptionWithUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; } [[OTRKit sharedInstance] initiateEncryptionWithUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol] asynchronously:YES]; } #pragma mark - #pragma mark Socialist Millionaire - (void)authenticateUser:(NSString *)messageTo from:(NSString *)messageFrom { NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; if (currentState == OTRKitMessageStateEncrypted) { [OTRKitAuthenticationDialog requestAuthenticationForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; } else { [self presentErrorMessage:TXTLS(@"OffTheRecord[jrv-gq]") withAccountName:messageTo]; } } #pragma mark - #pragma mark Encryption & Decryption - (void)decryptMessage:(NSString *)messageBody from:(NSString *)messageFrom to:(NSString *)messageTo decodingCallback:(nullable TLOEncryptionManagerEncodingDecodingCallbackBlock)decodingCallback { NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); NSParameterAssert(messageBody != nil); TLOEncryptionManagerEncodingDecodingObject *messageObject = [TLOEncryptionManagerEncodingDecodingObject new]; messageObject.messageTo = messageTo; messageObject.messageFrom = messageFrom; messageObject.messageBody = messageBody; messageObject.encodingCallback = decodingCallback; /* Messages are decoded synchronously because many things incoming from IRC are never passed through OTRKit. To keep the incoming queue synchronous, we keep it synchronous here. */ [[OTRKit sharedInstance] decodeMessage:messageBody username:messageFrom accountName:messageTo protocol:[self otrKitProtocol] asynchronously:NO tag:messageObject]; } - (void)encryptMessage:(NSString *)messageBody from:(NSString *)messageFrom to:(NSString *)messageTo encodingCallback:(nullable TLOEncryptionManagerEncodingDecodingCallbackBlock)encodingCallback injectionCallback:(nullable TLOEncryptionManagerInjectCallbackBlock)injectionCallback { NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); NSParameterAssert(messageBody != nil); TLOEncryptionManagerEncodingDecodingObject *messageObject = [TLOEncryptionManagerEncodingDecodingObject new]; messageObject.messageTo = messageTo; messageObject.messageFrom = messageFrom; messageObject.messageBody = messageBody; messageObject.encodingCallback = encodingCallback; messageObject.injectionCallback = injectionCallback; [[OTRKit sharedInstance] encodeMessage:messageBody tlvs:nil username:messageTo accountName:messageFrom protocol:[self otrKitProtocol] asynchronously:NO tag:messageObject]; } #pragma mark - #pragma mark Helper Methods - (OTRKitMessageState)messageStateFor:(NSString *)messageTo from:(NSString *)messageFrom { NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; return currentState; } - (BOOL)safeToTransferFile:(NSString *)filename to:(NSString *)messageTo from:(NSString *)messageFrom isIncomingFileTransfer:(BOOL)isIncomingFileTransfer { NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; if (currentState == OTRKitMessageStateEncrypted) { if (isIncomingFileTransfer) { BOOL continueop = [TDCAlert modalAlertWithMessage:TXTLS(@"OffTheRecord[nuo-8s]") title:TXTLS(@"OffTheRecord[mnw-qt]", filename) defaultButton:TXTLS(@"Prompts[qso-2g]") alternateButton:TXTLS(@"OffTheRecord[ng0-5q]")]; return (continueop == NO); } else { NSString *nickname = [self nicknameFromAccountName:messageTo]; BOOL continueop = [TDCAlert modalAlertWithMessage:TXTLS(@"OffTheRecord[7he-76]") title:TXTLS(@"OffTheRecord[gcg-tv]", filename, nickname) defaultButton:TXTLS(@"Prompts[qso-2g]") alternateButton:TXTLS(@"OffTheRecord[0v0-ix]")]; return (continueop == NO); } } return YES; } - (void)updateLockIconButton:(TVCMainWindowTitlebarAccessoryViewLockButton *)button withStateOf:(NSString *)messageTo from:(NSString *)messageFrom { NSParameterAssert(button != nil); NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); OTRKitMessageState currentState = [[OTRKit sharedInstance] messageStateForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; if (currentState == OTRKitMessageStateEncrypted) { BOOL hasVerifiedKey = [[OTRKit sharedInstance] activeFingerprintIsVerifiedForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; if (hasVerifiedKey) { button.title = TXTLS(@"OffTheRecord[l9n-p9]"); [button setIconAsLocked]; } else { button.title = TXTLS(@"OffTheRecord[w34-mg]"); /* Even though we are encrypted, our icon is still set to unlocked because the identity of messageTo still has not been authenticated. */ [button setIconAsUnlocked]; } } else { button.title = TXTLS(@"OffTheRecord[anu-ky]"); [button setIconAsUnlocked]; } } - (void)performBlock:(void (^)(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel))block inRelationToAccountName:(NSString *)accountName { [self performBlock:block inRelationToAccountName:accountName createWindowIfMissing:NO]; } - (void)performBlock:(void (^)(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel))block inRelationToAccountName:(NSString *)accountName createWindowIfMissing:(BOOL)createWindowIfMissing { XRPerformBlockSynchronouslyOnMainQueue(^{ IRCClient *client = [self connectionFromAccountName:accountName]; if (client == nil) { return; } NSString *nickname = [self nicknameFromAccountName:accountName]; IRCChannel *channel = nil; if (createWindowIfMissing) { channel = [client findChannelOrCreate:nickname isPrivateMessage:YES]; } else { channel = [client findChannel:nickname]; } block(nickname, client, channel); }); } - (nullable NSString *)localizedStringForEvent:(OTRKitMessageEvent)event { NSString *localeKey = nil; #define _dv(event, localInt) case (event): { localeKey = (localInt); break; } switch (event) { _dv(OTRKitMessageEventEncryptionRequired, @"jww-e0") _dv(OTRKitMessageEventEncryptionError, @"jvt-fi") _dv(OTRKitMessageEventConnectionEnded, @"c5o-2l") _dv(OTRKitMessageEventSetupError, @"uhy-85") _dv(OTRKitMessageEventMessageReflected, @"bl0-5i") _dv(OTRKitMessageEventMessageResent, @"c49-q0") _dv(OTRKitMessageEventReceivedMessageNotInPrivate, @"6v9-w3") _dv(OTRKitMessageEventReceivedMessageUnreadable, @"9if-xp") _dv(OTRKitMessageEventReceivedMessageMalformed, @"auo-n0") _dv(OTRKitMessageEventLogHeartbeatReceived, @"nl1-nf") _dv(OTRKitMessageEventLogHeartbeatSent, @"iwt-9f") _dv(OTRKitMessageEventReceivedMessageGeneralError, @"2zx-p6") _dv(OTRKitMessageEventReceivedMessageUnencrypted, @"1lf-f0") _dv(OTRKitMessageEventReceivedMessageUnrecognized, @"4by-8j") _dv(OTRKitMessageEventReceivedMessageForOtherInstance, @"c7t-vi") default: { break; } } #undef _dv if (localeKey) { localeKey = [NSString stringWithFormat:@"OffTheRecord[%@]", localeKey]; return TXTLS(localeKey); } return nil; } - (BOOL)eventIsErroneous:(OTRKitMessageEvent)event { switch (event) { case OTRKitMessageEventEncryptionError: case OTRKitMessageEventReceivedMessageGeneralError: case OTRKitMessageEventReceivedMessageMalformed: case OTRKitMessageEventReceivedMessageNotInPrivate: case OTRKitMessageEventReceivedMessageUnreadable: case OTRKitMessageEventReceivedMessageUnrecognized: case OTRKitMessageEventEncryptionRequired: { return YES; } default: { return NO; } } } - (void)printMessage:(NSString *)message inChannel:(IRCChannel *)channel onClient:(IRCClient *)client { [self printMessage:message inChannel:channel onClient:client escapeMessage:YES]; } - (void)printMessage:(NSString *)message inChannel:(IRCChannel *)channel onClient:(IRCClient *)client escapeMessage:(BOOL)escapeMessage { NSParameterAssert(message != nil); NSParameterAssert(channel != nil); NSParameterAssert(client != nil); [client print:message by:nil inChannel:channel asType:TVCLogLineTypeOffTheRecordEncryptionStatus command:TVCLogLineDefaultCommandValue escapeMessage:escapeMessage]; } - (void)presentMessage:(NSString *)message withAccountName:(NSString *)accountName { [self presentMessage:message withAccountName:accountName escapeMessage:YES]; } - (void)presentMessage:(NSString *)message withAccountName:(NSString *)accountName escapeMessage:(BOOL)escapeMessage { [self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) { if (channel == nil) { return; } [self printMessage:message inChannel:channel onClient:client escapeMessage:escapeMessage]; } inRelationToAccountName:accountName createWindowIfMissing:YES]; } - (void)presentErrorMessage:(NSString *)errorMessage withAccountName:(NSString *)accountName { [self presentErrorMessage:errorMessage withAccountName:accountName escapeMessage:YES]; } - (void)presentErrorMessage:(NSString *)errorMessage withAccountName:(NSString *)accountName escapeMessage:(BOOL)escapeMessage { [self presentMessage:errorMessage withAccountName:accountName escapeMessage:escapeMessage]; } - (void)authenticationStatusChangedForAccountName:(NSString *)accountName isVerified:(BOOL)isVerified { [self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) { if (channel == nil) { return; } if (isVerified) { [self printMessage:TXTLS(@"OffTheRecord[rj0-ys]", nickname) inChannel:channel onClient:client]; } else { [self printMessage:TXTLS(@"OffTheRecord[ufr-mh]", nickname) inChannel:channel onClient:client]; } [channel noteEncryptionStateDidChange]; } inRelationToAccountName:accountName]; } #pragma mark - #pragma mark Off-the-Record Kit Delegate - (void)updatePolicy { if ([TPCPreferences textEncryptionIsEnabled] == NO) { [OTRKit sharedInstance].otrPolicy = OTRKitPolicyNever; return; } if ([TPCPreferences textEncryptionIsRequired]) { [OTRKit sharedInstance].otrPolicy = OTRKitPolicyAlways; } else if ([TPCPreferences textEncryptionIsOpportunistic]) { [OTRKit sharedInstance].otrPolicy = OTRKitPolicyOpportunistic; } else { [OTRKit sharedInstance].otrPolicy = OTRKitPolicyManual; } } - (NSString *)otrKitProtocol { return @"prpl-irc"; } - (int)otrKitProtocolMaximumMessageSize { return 400; // Chosen by fair dice roll. } - (NSString *)maybeInsertProperNegotiationMessage:(NSString *)message { static NSRegularExpression *boundryRegex = nil; if (boundryRegex == nil) { NSString *boundryMatch = [NSString stringWithFormat: @"\\?OTRv?([0-9]+)\\?\n(.*) has requested an " @"Off-the-Record " @"private conversation. However, you do not have a plugin " @"to support that.\nSee " @"https://otr.cypherpunks.ca/ for more information."]; boundryRegex = [NSRegularExpression regularExpressionWithPattern:boundryMatch options:0 error:NULL]; } NSUInteger numberOfMatches = [boundryRegex numberOfMatchesInString:message options:0 range:message.range]; if (numberOfMatches == 1) { NSArray *messageComponents = [message componentsSeparatedByString:@"\n"]; return [NSString stringWithFormat:@"%@ %@", messageComponents[0], TXTLS(@"OffTheRecord[8hr-8l]")]; } return message; } - (void)otrKit:(OTRKit *)otrKit injectMessage:(NSString *)message username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol tag:(nullable id)tag { message = [self maybeInsertProperNegotiationMessage:message]; if (tag && [tag isKindOfClass:[TLOEncryptionManagerEncodingDecodingObject class]]) { TLOEncryptionManagerEncodingDecodingObject *messageObject = tag; if (messageObject.injectionCallback) { messageObject.injectionCallback(message); return; // Do not continue after callback block... } } [self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) { [client send:@"PRIVMSG", nickname, message, nil]; } inRelationToAccountName:username]; } - (void)otrKit:(OTRKit *)otrKit encodedMessage:(nullable NSString *)encodedMessage wasEncrypted:(BOOL)wasEncrypted username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol tag:(nullable id)tag error:(nullable NSError *)error { if (tag == nil || [tag isKindOfClass:[TLOEncryptionManagerEncodingDecodingObject class]] == NO) { return; } TLOEncryptionManagerEncodingDecodingObject *messageObject = tag; if (messageObject.encodingCallback) { messageObject.encodingCallback(messageObject.messageBody, wasEncrypted); } } - (void)otrKit:(OTRKit *)otrKit decodedMessage:(nullable NSString *)decodedMessage wasEncrypted:(BOOL)wasEncrypted tlvs:(NSArray *)tlvs username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol tag:(nullable id)tag { if (decodedMessage == nil) { return; } if (tag == nil || [tag isKindOfClass:[TLOEncryptionManagerEncodingDecodingObject class]] == NO) { return; } TLOEncryptionManagerEncodingDecodingObject *messageObject = tag; if (messageObject.encodingCallback) { messageObject.encodingCallback(decodedMessage, wasEncrypted); } } - (void)otrKit:(OTRKit *)otrKit updateMessageState:(OTRKitMessageState)messageState username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol { /* We do not force create window if it does not exist when updating encryption status because status changes are only important if one is open. When a new window is created, it will populate the latest state regardless of delegate. */ [self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) { if (channel == nil) { return; } [channel noteEncryptionStateDidChange]; } inRelationToAccountName:username]; if (messageState == OTRKitMessageStateEncrypted) { BOOL isVerified = [[OTRKit sharedInstance] activeFingerprintIsVerifiedForUsername:username accountName:accountName protocol:[self otrKitProtocol]]; if (isVerified) { [self presentMessage:TXTLS(@"OffTheRecord[r3w-fj]") withAccountName:username]; } else { [self presentMessage:TXTLS(@"OffTheRecord[l49-9y]") withAccountName:username escapeMessage:NO]; } } else if (messageState == OTRKitMessageStateFinished || messageState == OTRKitMessageStatePlaintext) { [self presentMessage:TXTLS(@"OffTheRecord[m1z-eb]") withAccountName:username]; /* When policy is changed to never, all open conversations are closed. Because user probably wont intend to use authentication dialogs without encryption enabled, then let's cancel any that are open for this condition. */ if ([TPCPreferences textEncryptionIsEnabled] == NO) { [OTRKitAuthenticationDialog cancelRequestForUsername:username accountName:accountName protocol:protocol]; } } } - (BOOL)otrKit:(OTRKit *)otrKit isUsernameLoggedIn:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol { __block BOOL userIsActive = NO; [self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) { if (channel == nil) { return; } userIsActive = channel.isActive; } inRelationToAccountName:username]; return userIsActive; } - (void)otrKit:(OTRKit *)otrKit showFingerprintConfirmationForTheirHash:(NSString *)theirHash ourHash:(NSString *)ourHash username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol { [self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) { /* We print this message unescaped to include an anchor in the HTML that the user can click to authenticate the user. We are passing outside input to it, which we do escape. */ [self printMessage:TXTLS(@"OffTheRecord[67n-6j]", [TVCLogRenderer escapeHTML:nickname], [TVCLogRenderer escapeHTML:theirHash]) inChannel:channel onClient:client escapeMessage:NO]; } inRelationToAccountName:username createWindowIfMissing:YES]; } - (void)otrKit:(OTRKit *)otrKit handleSMPEvent:(OTRKitSMPEvent)event progress:(double)progress question:(nullable NSString *)question username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol error:(nullable NSError *)error { [OTRKitAuthenticationDialog handleAuthenticationRequest:event progress:progress question:question username:username accountName:accountName protocol:protocol]; } - (void)otrKit:(OTRKit *)otrKit handleMessageEvent:(OTRKitMessageEvent)event message:(NSString *)message username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol tag:(nullable id)tag error:(nullable NSError *)error { if (event == OTRKitMessageEventReceivedMessageUnencrypted) { [self otrKit:otrKit decodedMessage:message wasEncrypted:NO tlvs:nil username:username accountName:accountName protocol:protocol tag:tag]; return; } if ([self eventIsErroneous:event]) { NSString *errorMessage = [self localizedStringForEvent:event]; [self presentErrorMessage:errorMessage withAccountName:username]; } } - (void)otrKit:(OTRKit *)otrKit receivedSymmetricKey:(NSData *)symmetricKey forUse:(NSUInteger)use useData:(NSData *)useData username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol { } - (void)otrKit:(OTRKit *)otrKit willStartGeneratingPrivateKeyForAccountName:(NSString *)accountName protocol:(NSString *)protocol { } - (void)otrKit:(OTRKit *)otrKit didFinishGeneratingPrivateKeyForAccountName:(NSString *)accountName protocol:(NSString *)protocol error:(nullable NSError *)error { } - (void)otrKit:(OTRKit *)otrKit fingerprintIsVerifiedStateChangedForUsername:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol verified:(BOOL)verified { [self authenticationStatusChangedForAccountName:username isVerified:verified]; } - (void)otrKitFingerprintManagerDialogDidClose:(OTRKitFingerprintManagerDialog *)otrkitFingerprintManager { self.fingerprintManagerDialog = nil; } - (BOOL)otrKit:(OTRKit *)otrKit ignoreMessage:(NSString *)message messageType:(OTRKitMessageType)messageType username:(NSString *)username accountName:(NSString *)accountName protocol:(NSString *)protocol { __block BOOL ignoreMessage = NO; [self performBlock:^(NSString *nickname, IRCClient *client, IRCChannel * _Nullable channel) { if (messageType == OTRKitMessageTypeNotOTR) { return; } if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityEchoMessage]) { ignoreMessage = [client nicknameIsMyself:nickname]; } } inRelationToAccountName:username]; return ignoreMessage; } #pragma mark - #pragma mark Menu Item Actions - (BOOL)validateMenuItem:(NSMenuItem *)menuItem withStateOf:(NSString *)messageTo from:(NSString *)messageFrom { NSParameterAssert(menuItem != nil); NSParameterAssert(messageTo != nil); NSParameterAssert(messageFrom != nil); NSUInteger menuItemTag = menuItem.tag; if (menuItemTag == MTOTRStatusButtonViewListOfFingerprints) { return YES; } OTRKitMessageState currentMessageState = [[OTRKit sharedInstance] messageStateForUsername:messageTo accountName:messageFrom protocol:[self otrKitProtocol]]; BOOL messageStateEncrypted = (currentMessageState == OTRKitMessageStateEncrypted); switch (menuItemTag) { case MTOTRStatusButtonStartPrivateConversation: { menuItem.hidden = messageStateEncrypted; return YES; } case MTOTRStatusButtonRefreshPrivateConversation: { menuItem.hidden = (messageStateEncrypted == NO); return YES; } case MTOTRStatusButtonEndPrivateConversation: { return messageStateEncrypted; } case MTOTRStatusButtonAuthenticateChatPartner: { return messageStateEncrypted; } } return NO; } @end #pragma mark - #pragma mark Dummy Class @implementation TLOEncryptionManagerEncodingDecodingObject @end #endif NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOFileLogger.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXGlobalModels.h" #import "TDCAlert.h" #import "TLOLocalization.h" #import "TLOTimer.h" #import "TPCPathInfoPrivate.h" #import "TPCPreferencesLocal.h" #import "IRCClient.h" #import "IRCChannel.h" #import "TVCLogLinePrivate.h" #import "TLOFileLoggerPrivate.h" NS_ASSUME_NONNULL_BEGIN /* How frequent to display alert for no disk space */ #define _noSpaceLeftOnDeviceAlertInterval 300 // 5 minutes /* How long a file handle is allowed to be idle */ #define _fileHandleIdleLimit 1200 // 20 minutes /* The frequency by which idle is checked for */ #define _idleTimerInterval 600 // 10 minutes NSString * const TLOFileLoggerConsoleDirectoryName = @"Console"; NSString * const TLOFileLoggerChannelDirectoryName = @"Channels"; NSString * const TLOFileLoggerPrivateMessageDirectoryName = @"Queries"; NSString * const TLOFileLoggerUndefinedNicknameFormat = @"<%@%n>"; NSString * const TLOFileLoggerActionNicknameFormat = @"\u2022 %n:"; NSString * const TLOFileLoggerNoticeNicknameFormat = @"-%n-"; NSString * const TLOFileLoggerISOStandardClockFormat = @"[%Y-%m-%dT%H:%M:%S%z]"; // 2008-07-09T16:13:30+12:00 NSString * const TLOFileLoggerIdleTimerNotification = @"TLOFileLoggerIdleTimerNotification"; @interface TLOFileLogger () @property (nonatomic, weak) IRCClient *client; @property (nonatomic, weak) IRCChannel *channel; @property (nonatomic, strong, nullable) NSFileHandle *fileHandle; @property (nonatomic, copy, readwrite, nullable) NSString *filePath; @property (readonly, copy, readonly, nullable) NSString *filePathComputed; @property (nonatomic, copy, nullable) NSDate *dateOpened; @property (nonatomic, assign) NSTimeInterval lastWriteTime; @property (readonly) BOOL fileHandleIdle; @property (readonly, class) TLOTimer *idleTimer; @end static NSUInteger _numberOfOpenFileHandles = 0; @implementation TLOFileLogger - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithClient:(IRCClient *)client { NSParameterAssert(client != nil); if ((self = [super init])) { self.client = client; return self; } return nil; } - (instancetype)initWithChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); if ((self = [super init])) { self.client = channel.associatedClient; self.channel = channel; return self; } return nil; } - (void)dealloc { [self close]; } #pragma mark - #pragma mark Plain Text API - (void)writeLogLine:(TVCLogLine *)logLine { NSParameterAssert(logLine != nil); NSString *stringToWrite = nil; if (self.channel) { stringToWrite = [logLine renderedBodyForTranscriptLogInChannel:self.channel]; } else { stringToWrite = [logLine renderedBodyForTranscriptLog]; } [self writePlainText:stringToWrite]; } - (void)writePlainText:(NSString *)string { NSParameterAssert(string != nil); [self reopenIfNeeded]; if (self.fileHandle == nil) { LogToConsoleError("File handle is closed"); return; } NSString *stringToWrite = [string stringByAppendingString:@"\n"]; NSData *dataToWrite = [stringToWrite dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; if (dataToWrite) { @try { self.lastWriteTime = [NSDate timeIntervalSince1970]; [self.fileHandle writeData:dataToWrite]; } @catch (NSException *exception) { LogToConsoleError("Caught exception: %{public}@", exception.reason); LogStackTrace(); if ([exception.reason contains:@"No space left on device"]) { [self failWithNoSpaceLeftOnDevice]; } [self close]; } // @catch } } #pragma mark - #pragma mark File Handle Management - (void)failWithNoSpaceLeftOnDevice { static BOOL alertVisible = NO; if (alertVisible) { return; } static NSTimeInterval lastFailTime = 0; NSTimeInterval currentTime = [NSDate timeIntervalSince1970]; if (lastFailTime > 0) { if ((currentTime - lastFailTime) < _noSpaceLeftOnDeviceAlertInterval) { return; } } lastFailTime = currentTime; alertVisible = YES; /* Present alert as non-blocking because there is no need for it to disrupt UI */ [TDCAlert alertWithMessage:TXTLS(@"Prompts[v9e-jy]") title:TXTLS(@"Prompts[bi7-ah]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { alertVisible = NO; }]; } - (void)reset { if (self.fileHandle == nil) { return; } [self.fileHandle truncateFileAtOffset:0]; } - (void)close { if (self.fileHandle == nil) { return; } @try { [self.fileHandle synchronizeFile]; } @catch (NSException *exception) { LogToConsoleError("Caught exception: %{public}@", exception.reason); LogStackTrace(); } [self.fileHandle closeFile]; self.fileHandle = nil; self.filePath = nil; self.lastWriteTime = 0; self.dateOpened = nil; [self removeIdleTimerObserver]; } - (void)reopenIfNeeded { /* Implementation discussion: we already have a notification called IRCWorldDateHasChangedNotification that is fired by IRCWorld when the user changes the date of the system or after time has naturally reached midnight. Do we rely on this? I chose not to and here is why: Race conditions. There is no guarantee we will receive notification on time to beat the current write. Of which there can be multiple. */ if (self.fileHandle != nil && [self.dateOpened isInSameDayAsDate:[NSDate date]]) { return; } [self reopen]; } - (void)reopen { [self close]; [self open]; } - (void)open { if (self.fileHandle != nil) { LogToConsoleError("Tried to open log file when a file handle already exists"); return; } if ([self buildFilePath] == NO) { return; } NSString *filePath = self.filePath; NSString *writePath = self.writePath; if ([RZFileManager() fileExistsAtPath:writePath] == NO) { NSError *createDirectoryError = nil; if ([RZFileManager() createDirectoryAtPath:writePath withIntermediateDirectories:YES attributes:nil error:&createDirectoryError] == NO) { LogToConsoleError("Error Creating Folder: %{public}@", createDirectoryError.localizedDescription); return; } } if ([RZFileManager() fileExistsAtPath:filePath] == NO) { NSError *writeFileError = nil; if ([@"" writeToFile:filePath atomically:NO encoding:NSUTF8StringEncoding error:&writeFileError] == NO) { LogToConsoleError("Error Creating File: %{public}@", writeFileError.localizedDescription); return; } } NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath]; if (fileHandle == nil) { LogToConsoleError("Failed to open file handle at path '%{public}@'", filePath.standardizedTildePath); return; } [fileHandle seekToEndOfFile]; self.fileHandle = fileHandle; self.dateOpened = [NSDate date]; [self addIdleTimerObserver]; } #pragma mark - #pragma mark Idle Timer - (BOOL)fileHandleIdle { NSTimeInterval lastWriteTime = self.lastWriteTime; NSTimeInterval currentTime = [NSDate timeIntervalSince1970]; if (lastWriteTime > 0) { if ((currentTime - lastWriteTime) > _fileHandleIdleLimit) { return YES; } } return NO; } + (TLOTimer *)idleTimer { static TLOTimer *idleTimer = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ idleTimer = [TLOTimer timerWithActionBlock:^(TLOTimer *sender) { [self idleTimerFired]; } onQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)]; }); return idleTimer; } + (void)idleTimerFired { if (_numberOfOpenFileHandles == 0) { [self stopIdleTimer]; return; } [RZNotificationCenter() postNotificationName:TLOFileLoggerIdleTimerNotification object:nil]; } + (void)startIdleTimer { TLOTimer *idleTimer = self.idleTimer; if (idleTimer.timerIsActive) { return; } [idleTimer start:_idleTimerInterval onRepeat:YES]; } + (void)stopIdleTimer { TLOTimer *idleTimer = self.idleTimer; if (idleTimer.timerIsActive == NO) { return; } [idleTimer stop]; } - (void)idleTimerFired:(NSNotification *)notification { if (self.fileHandleIdle == NO) { return; } LogToConsoleDebug("Closing %{public}@ because it's idle", self); [self close]; } - (void)updateIdleTimer { if (_numberOfOpenFileHandles == 0) { [self.class stopIdleTimer]; } else { [self.class startIdleTimer]; } } - (void)addIdleTimerObserver { _numberOfOpenFileHandles += 1; [RZNotificationCenter() addObserver:self selector:@selector(idleTimerFired:) name:TLOFileLoggerIdleTimerNotification object:nil]; [self updateIdleTimer]; } - (void)removeIdleTimerObserver { _numberOfOpenFileHandles -= 1; [RZNotificationCenter() removeObserver:self name:TLOFileLoggerIdleTimerNotification object:nil]; [self updateIdleTimer]; } #pragma mark - #pragma mark Paths - (nullable NSString *)writePath { return self.filePath.stringByDeletingLastPathComponent; } - (nullable NSString *)fileName { return self.filePath.lastPathComponent; } + (nullable NSString *)writePathForItem:(IRCTreeItem *)item { NSParameterAssert(item != nil); NSString *sourcePath = [TPCPathInfo transcriptFolder]; if (sourcePath == nil) { return nil; } return [self writePathForItem:item relativeTo:sourcePath]; } + (nullable NSString *)writePathForItem:(IRCTreeItem *)item relativeTo:(NSString *)sourcePath { NSParameterAssert(sourcePath != nil); NSParameterAssert(item != nil); IRCChannel *channel = item.associatedChannel; if (channel && channel.isUtility) { return nil; } IRCClient *client = item.associatedClient; NSString *clientIdentifier = [client.uniqueIdentifier substringToIndex:5]; NSString *clientName = [NSString stringWithFormat:@"%@ (%@)", client.name, clientIdentifier]; NSString *basePath = nil; if (channel == nil) { basePath = [NSString stringWithFormat:@"/%@/%@/", clientName.safeFilename, TLOFileLoggerConsoleDirectoryName]; } else if (channel.isChannel) { basePath = [NSString stringWithFormat:@"/%@/%@/%@/", clientName.safeFilename, TLOFileLoggerChannelDirectoryName, channel.name.safeFilename]; } else if (channel.isPrivateMessage) { basePath = [NSString stringWithFormat:@"/%@/%@/%@/", clientName.safeFilename, TLOFileLoggerPrivateMessageDirectoryName, channel.name.safeFilename]; } return [sourcePath stringByAppendingPathComponent:basePath]; } - (nullable NSString *)writePathRelativeTo:(NSString *)sourcePath { NSParameterAssert(sourcePath != nil); IRCClient *client = self.client; IRCChannel *channel = self.channel; IRCTreeItem *item = ((channel) ?: client); return [self.class writePathForItem:item relativeTo:sourcePath]; } - (BOOL)buildFilePath { NSString *sourcePath = [TPCPathInfo transcriptFolder]; if (sourcePath == nil) { return NO; } NSString *writePath = [self writePathRelativeTo:sourcePath]; if (writePath == nil) { return NO; } NSString *dateTime = TXFormattedTimestamp([NSDate date], @"%Y-%m-%d"); NSString *fileName = [NSString stringWithFormat:@"%@.txt", dateTime]; NSString *filePath = [writePath stringByAppendingPathComponent:fileName]; self.filePath = filePath; return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOInputHistory.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXMasterController.h" #import "TPCPreferencesLocal.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCWorld.h" #import "TVCMainWindow.h" #import "TVCMainWindowTextView.h" #import "TLOInputHistoryPrivate.h" NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Private Interface #define _inputHistoryMax 100 NSString * const _inputHistoryGlobalObjectKey = @"TLOInputHistoryDefaultObject"; @interface TLOInputHistory () @property (nonatomic, weak) TVCMainWindow *window; @property (nonatomic, strong) NSMutableDictionary *historyObjects; @property (nonatomic, copy, nullable) NSString *currentTreeItem; @end @interface TLOInputHistoryObject : NSObject @property (nonatomic, assign) NSInteger historyBufferPosition; @property (nonatomic, strong) NSMutableArray *historyBuffer; @property (nonatomic, copy, nullable) NSAttributedString *lastHistoryItem; - (void)add:(NSAttributedString *)string; - (nullable NSAttributedString *)up:(NSAttributedString *)string; - (nullable NSAttributedString *)down:(NSAttributedString *)string; @end #pragma mark - #pragma mark Input History Manager @implementation TLOInputHistory - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithWindow:(TVCMainWindow *)mainWindow { NSParameterAssert(mainWindow != nil); if ((self = [super init])) { self.window = mainWindow; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.historyObjects = [NSMutableDictionary dictionary]; } - (void)destroy:(IRCTreeItem *)treeItem { NSParameterAssert(treeItem != nil); if ([TPCPreferences inputHistoryIsChannelSpecific] == NO) { return; } @synchronized(self.historyObjects) { if (treeItem.isClient) { for (IRCChannel *treeItemChild in ((IRCClient *)treeItem).channelList) { [self destroy:treeItemChild]; } } NSString *itemId = treeItem.uniqueIdentifier; [self.historyObjects removeObjectForKey:itemId]; if ([self.currentTreeItem isEqualToString:itemId]) { self.currentTreeItem = nil; } } } - (void)moveFocusTo:(IRCTreeItem *)treeItem { NSParameterAssert(treeItem != nil); if ([TPCPreferences inputHistoryIsChannelSpecific] == NO) { return; } TVCMainWindowTextView *textView = self.window.inputTextField; /* Set current text field value to current object. */ TLOInputHistoryObject *oldObject = [self currentObjectForFocusedTreeView]; if (oldObject) { oldObject.lastHistoryItem = textView.attributedStringValue; } /* Change to new view */ self.currentTreeItem = treeItem.uniqueIdentifier; /* Does new selection have a history item? */ TLOInputHistoryObject *newObject = [self currentObjectForFocusedTreeView]; NSAttributedString *lastHistoryItem = newObject.lastHistoryItem; if (lastHistoryItem) { textView.attributedStringValue = lastHistoryItem; } else { textView.stringValue = @""; } } - (void)noteInputHistoryObjectScopeDidChange { @synchronized(self.historyObjects) { /* If the input history was made channel specific, then we copy the current value of the global input history to all tree items. */ if ([TPCPreferences inputHistoryIsChannelSpecific]) { for (IRCClient *u in worldController().clientList) { [self inputHistoryObjectScopeDidChangeApplyToItem:u.uniqueIdentifier]; for (IRCChannel *c in u.channelList) { [self inputHistoryObjectScopeDidChangeApplyToItem:c.uniqueIdentifier]; } } [self.historyObjects removeObjectForKey:_inputHistoryGlobalObjectKey]; } else { [self.historyObjects removeAllObjects]; self.currentTreeItem = nil; } } } - (void)inputHistoryObjectScopeDidChangeApplyToItem:(NSString *)itemId { NSParameterAssert(itemId != nil); TLOInputHistoryObject *globalObject = self.historyObjects[_inputHistoryGlobalObjectKey]; if (globalObject) { TLOInputHistoryObject *newObject = [globalObject copy]; newObject.lastHistoryItem = nil; self.historyObjects[itemId] = newObject; } } - (nullable TLOInputHistoryObject *)currentObjectForFocusedTreeView { @synchronized(self.historyObjects) { NSString *currentObjectKey = nil; if ([TPCPreferences inputHistoryIsChannelSpecific]) { currentObjectKey = self.currentTreeItem; } else { currentObjectKey = _inputHistoryGlobalObjectKey; } if (currentObjectKey == nil) { return nil; } TLOInputHistoryObject *currentObject = self.historyObjects[currentObjectKey]; if (currentObject == nil) { currentObject = [TLOInputHistoryObject new]; self.historyObjects[currentObjectKey] = currentObject; } return currentObject; } } - (void)add:(NSAttributedString *)string { TLOInputHistoryObject *object = [self currentObjectForFocusedTreeView]; if (object == nil) { return; } [object add:string]; } - (nullable NSAttributedString *)up:(NSAttributedString *)string { TLOInputHistoryObject *object = [self currentObjectForFocusedTreeView]; if (object == nil) { return nil; } return [object up:string]; } - (nullable NSAttributedString *)down:(NSAttributedString *)string { TLOInputHistoryObject *object = [self currentObjectForFocusedTreeView]; if (object == nil) { return nil; } return [object down:string]; } @end #pragma mark - #pragma mark Input History Objects @implementation TLOInputHistoryObject - (id)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.historyBuffer = [NSMutableArray new]; } - (void)add:(NSAttributedString *)string { NSParameterAssert(string != nil); if (string.length == 0) { return; } @synchronized(self.historyBuffer) { NSAttributedString *lastEntry = self.historyBuffer.lastObject; if (lastEntry != nil || [lastEntry.string isEqualToString:string.string] == NO) { [self addToBuffer:string]; } self.historyBufferPosition = self.historyBuffer.count; } } - (void)addToBuffer:(NSAttributedString *)string { NSParameterAssert(string != nil); [self.historyBuffer addObject:string]; if (self.historyBuffer.count > _inputHistoryMax) { [self.historyBuffer removeObjectAtIndex:0]; } } - (nullable NSAttributedString *)up:(NSAttributedString *)string { NSParameterAssert(string != nil); @synchronized(self.historyBuffer) { if (string.length > 0) { NSAttributedString *lastEntry = [self entryAtBufferPosition]; if (lastEntry == nil || [lastEntry.string isEqualToString:string.string] == NO) { [self addToBuffer:string]; } } self.historyBufferPosition -= 1; if (self.historyBufferPosition < 0) { self.historyBufferPosition = 0; } else if (self.historyBufferPosition < self.historyBuffer.count) { return self.historyBuffer[self.historyBufferPosition]; } return nil; } } - (nullable NSAttributedString *)down:(NSAttributedString *)string { NSParameterAssert(string != nil); @synchronized(self.historyBuffer) { if (string.length == 0) { self.historyBufferPosition = self.historyBuffer.count; return nil; } NSAttributedString *lastEntry = [self entryAtBufferPosition]; if (lastEntry == nil || [lastEntry.string isEqualToString:string.string] == NO) { [self addToBuffer:string]; return [NSAttributedString attributedString]; } self.historyBufferPosition += 1; lastEntry = [self entryAtBufferPosition]; if (lastEntry) { return lastEntry; } return [NSAttributedString attributedString]; } } - (BOOL)bufferPositionIsInRange { return (self.historyBufferPosition >= 0 && self.historyBufferPosition < self.historyBuffer.count); } - (nullable NSAttributedString *)entryAtBufferPosition { if ([self bufferPositionIsInRange] == NO) { return nil; } return self.historyBuffer[self.historyBufferPosition]; } - (id)copyWithZone:(nullable NSZone *)zone { TLOInputHistoryObject *newObject = [TLOInputHistoryObject new]; [newObject->_historyBuffer addObjectsFromArray:self->_historyBuffer]; newObject->_historyBufferPosition = self->_historyBufferPosition; newObject->_lastHistoryItem = self->_lastHistoryItem; return newObject; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOInternetAddressLookup.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TPCPreferencesLocal.h" #import "TLOInternetAddressLookup.h" NS_ASSUME_NONNULL_BEGIN #define _requestTimeoutInterval 30.0 @interface TLOInternetAddressLookup () @property (nonatomic, weak) id requestDelegate; @property (nonatomic, strong) NSURLConnection *connection; @property (nonatomic, strong) NSURLResponse *connectionResponse; @property (nonatomic, strong) NSMutableData *connectionResponseData; @property (nonatomic, copy, nullable) NSString *address; @end @implementation TLOInternetAddressLookup #pragma mark - #pragma mark Public API - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithDelegate:(id )delegate { NSParameterAssert(delegate != nil); if ((self = [super init])) { self.IPv4AddressIsValid = YES; self.IPv6AddressIsValid = YES; self.requestDelegate = delegate; return self; } return nil; } - (void)performLookup { [self setupConnectionRequest]; } - (void)cancelLookup { [self _teardownConnectionRequest]; } - (void)setupConnectionRequest { NSAssert((self.connection == nil), @"A lookup is already in progress"); self.connectionResponseData = [NSMutableData data]; NSURL *requestURL = [NSURL URLWithString:[self addressSourceURL]]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:_requestTimeoutInterval]; request.HTTPMethod = @"GET"; self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; } - (void)_teardownConnectionRequest { if (self.connection) { [self.connection cancel]; } self.connection = nil; self.connectionResponse = nil; self.connectionResponseData = nil; } - (void)teardownConnectionRequest { [self _teardownConnectionRequest]; [self informDelegate]; self.address = nil; } - (NSString *)addressSourceURL { if ([TPCPreferences fileTransferIPAddressDetectionMethod] == TXFileTransferIPAddressMethodRouterAndThirdParty) { return [self thirdPartySourceURL]; } return @"https://myip.codeux.com/"; } - (NSString *)thirdPartySourceURL { NSArray *services = @[ @"https://wtfismyip.com/text", @"https://canhazip.com/", @"http://ifconfig.me/ip", @"http://v4.ipv6-test.com/api/myip.php", ]; NSUInteger randomIndex = (arc4random() % services.count); return services[randomIndex]; } #pragma mark - #pragma mark Connection Delegate - (void)informDelegate { NSString *address = self.address; if (address) { [self informDelegateLookupReturnedAddress:address]; } else { [self informDelegateLookupFailed]; } } - (void)informDelegateLookupReturnedAddress:(NSString *)address { if ([self.requestDelegate respondsToSelector:@selector(internetAddressLookupReturnedAddress:)]) { [self.requestDelegate internetAddressLookupReturnedAddress:address]; } } - (void)informDelegateLookupFailed { if ([self.requestDelegate respondsToSelector:@selector(internetAddressLookupFailed)]) { [self.requestDelegate internetAddressLookupFailed]; } } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { id connectionResponse = self.connectionResponse; // connectionResponse may not be NSHTTPURLResponse if the website // requested performs a location redirect to a data resource. if ([connectionResponse isKindOfClass:[NSHTTPURLResponse class]]) { BOOL isValidResponse = ([connectionResponse statusCode] == 200); if (isValidResponse) { NSData *addressData = self.connectionResponseData; NSString *address = [NSString stringWithData:addressData encoding:NSUTF8StringEncoding]; address = [address stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if ((address.isIPv4Address && self.IPv4AddressIsValid) || (address.isIPv6Address && self.IPv6AddressIsValid)) { self.address = address; } } } [self teardownConnectionRequest]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { LogToConsole("Lookup failed with error: %{public}@", error.localizedDescription); [self teardownConnectionRequest]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.connectionResponseData appendData:data]; // There is no reasonable explanation for the content of a request, // without headers, to exceed this length when it's sent in plain text. if (self.connectionResponseData.length > 1024) { LogToConsoleError("Too much data has been received for this to be a valid request"); [self teardownConnectionRequest]; } } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { self.connectionResponse = response; } - (nullable NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { return nil; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOKeyEventHandler.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "NSObjectHelperPrivate.h" #import "TLOKeyEventHandler.h" NS_ASSUME_NONNULL_BEGIN @interface TLOKeyEventHandler () @property (nonatomic, unsafe_unretained) id target; @property (nonatomic, strong) NSMutableDictionary *codeHandlerMap; @property (nonatomic, strong) NSMutableDictionary *characterHandlerMap; @end @implementation TLOKeyEventHandler - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithTarget:(id)target { if ((self = [super init])) { [self setKeyHandlerTarget:target]; [self prepareInitialState]; } return self; } - (void)prepareInitialState { self.characterHandlerMap = [NSMutableDictionary new]; self.codeHandlerMap = [NSMutableDictionary new]; } - (void)dealloc { self.target = nil; } - (void)setKeyHandlerTarget:(id)target { NSParameterAssert(target != nil); self.target = target; } - (void)registerSelector:(SEL)selector key:(NSUInteger)keyCode modifiers:(NSUInteger)modifiers { NSParameterAssert(selector != NULL); NSParameterAssert(keyCode != 0); NSNumber *modifierKeys = @(modifiers); NSMutableDictionary *map = self.codeHandlerMap[modifierKeys]; if (map == nil) { map = [NSMutableDictionary dictionary]; self.codeHandlerMap[modifierKeys] = map; } map[@(keyCode)] = NSStringFromSelector(selector); } - (void)registerSelector:(SEL)selector character:(UniChar)character modifiers:(NSUInteger)modifiers { NSParameterAssert(selector != NULL); NSParameterAssert(character != 0); NSNumber *modifierKeys = @(modifiers); NSMutableDictionary *map = self.characterHandlerMap[modifierKeys]; if (map == nil) { map = [NSMutableDictionary dictionary]; self.characterHandlerMap[modifierKeys] = map; } map[@(character)] = NSStringFromSelector(selector); } - (void)registerSelector:(SEL)selector characters:(NSRange)characterRange modifiers:(NSUInteger)modifiers { NSParameterAssert(selector != NULL); NSNumber *modifierKeys = @(modifiers); NSMutableDictionary *map = self.characterHandlerMap[modifierKeys]; if (map == nil) { map = [NSMutableDictionary dictionary]; self.characterHandlerMap[modifierKeys] = map; } NSUInteger from = characterRange.location; NSUInteger to = NSMaxRange(characterRange); for (NSInteger i = from; i < to; ++i) { map[@(i)] = NSStringFromSelector(selector); } } - (BOOL)processKeyEvent:(NSEvent *)e { NSParameterAssert(e != nil); NSTextInputContext *inputContext = [NSTextInputContext currentInputContext]; if (inputContext && [inputContext.client markedRange].length > 0) { return NO; } NSUInteger modifiers = (e.modifierFlags & (NSEventModifierFlagShift | NSEventModifierFlagControl | NSEventModifierFlagOption | NSEventModifierFlagCommand)); NSNumber *modifierKeys = @(modifiers); NSMutableDictionary *codeMap = self.codeHandlerMap[modifierKeys]; if (codeMap) { NSString *selectorName = codeMap[@(e.keyCode)]; if (selectorName) { SEL selector = NSSelectorFromString(selectorName); NSMethodSignature *signature = [self.target methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self.target]; [invocation setSelector:selector]; [invocation setArgument:&e atIndex:2]; [invocation invoke]; return YES; } } NSMutableDictionary *characterMap = self.characterHandlerMap[modifierKeys]; if (characterMap) { NSString *characterString = e.charactersIgnoringModifiers.lowercaseString; if (characterString.length > 0) { NSString *selectorName = characterMap[@([characterString characterAtIndex:0])]; if (selectorName) { SEL selector = NSSelectorFromString(selectorName); NSMethodSignature *signature = [self.target methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self.target]; [invocation setSelector:selector]; [invocation setArgument:&e atIndex:2]; [invocation invoke]; return YES; } } } return NO; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOLinkParser.swift ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ @objc(TLOLinkParser) public class LinkParser: NSObject { @objc(locateLinksInString:) public static func locateLinks(in string: String) -> [AHHyperlinkScannerResult] { return AHHyperlinkScanner.matches(in: string, strictMatching: false) } @objc public static let bannedLineTypes = [ TVCLogLine.string(for: .mode), TVCLogLine.string(for: .join), TVCLogLine.string(for: .nick), TVCLogLine.string(for: .invite) ].compactMap { $0 } } ================================================ FILE: Sources/App/Classes/Library/TLONicknameCompletionStatus.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCChannelUser.h" #import "IRCCommandIndex.h" #import "IRCISupportInfo.h" #import "IRCUser.h" #import "THOPluginManagerPrivate.h" #import "TPCApplicationInfo.h" #import "TPCPreferencesLocal.h" #import "TVCMainWindow.h" #import "TVCMainWindowTextView.h" #import "TLONicknameCompletionStatusPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TLONicknameCompletionStatus () @property (nonatomic, weak) TVCMainWindow *window; @property (nonatomic, copy, nullable) NSString *completedValue; @property (nonatomic, copy, nullable) NSString *completedValueCompletionSuffix; @property (nonatomic, copy, nullable) NSString *currentTextViewStringValue; @property (nonatomic, copy, nullable) NSString *cachedSearchPattern; @property (nonatomic, copy, nullable) NSString *cachedSearchPatternPrefixCharacter; @property (nonatomic, copy, nullable) NSString *cachedCompletionSuffix; @property (nonatomic, assign) NSRange selectionRangeAfterLastCompletion; @property (nonatomic, assign) NSRange rangeOfTextSelection; @property (nonatomic, assign) NSRange rangeOfSearchPattern; @property (nonatomic, assign) NSRange rangeOfCompletionSuffix; @property (nonatomic, assign) NSInteger selectionIndexOfLastCompletion; @property (nonatomic, assign) BOOL completionIsMovingForward; @property (nonatomic, assign) BOOL isCompletingChannelName; @property (nonatomic, assign) BOOL isCompletingCommand; @property (nonatomic, assign) BOOL isCompletingNickname; @property (nonatomic, assign) BOOL searchPatternIsAtStart; @property (nonatomic, assign) BOOL searchPatternIsAtEnd; @property (nonatomic, assign) BOOL completionCacheIsConstructed; @end @implementation TLONicknameCompletionStatus - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithWindow:(TVCMainWindow *)mainWindow { if ((self = [super init])) { self.window = mainWindow; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [self clear]; } - (void)completeNickname:(BOOL)movingForward { [self performCompletion:movingForward]; } #pragma mark - #pragma mark Completion Management - (void)performCompletion:(BOOL)movingForward { BOOL canContinuePreviousScan = YES; TVCMainWindowTextView *textView = self.window.inputTextField; /* Focus text field so if the insertion point we not already in it, its there now so we can get selected range. */ [textView focus]; /* Get the selected range. Length may be zero in which case the insertion point is just sitting idle. */ NSRange selectedRange = [textView selectedRange]; if (selectedRange.location == NSNotFound) { return; } /* Perform various comparisons to determine whether the cache has to be rebuilt. */ if ( self.selectionIndexOfLastCompletion == NSNotFound || self.selectionRangeAfterLastCompletion.location == NSNotFound || (self.rangeOfTextSelection.location == NSNotFound && self.rangeOfSearchPattern.location == 0 && self.rangeOfSearchPattern.length == 0) || selectedRange.location != self.selectionRangeAfterLastCompletion.location || selectedRange.length != self.selectionRangeAfterLastCompletion.length) { canContinuePreviousScan = NO; } NSString *currentTextViewStringValue = textView.string; if (self.currentTextViewStringValue.length == 0) { canContinuePreviousScan = NO; } else if ([self.currentTextViewStringValue isEqualToString:currentTextViewStringValue] == NO) { canContinuePreviousScan = NO; } self.currentTextViewStringValue = currentTextViewStringValue; self.completionIsMovingForward = movingForward; /* Move onto stage two of the completion */ if (canContinuePreviousScan == NO) { self.rangeOfTextSelection = selectedRange; [self constructCache]; } if (self.completionCacheIsConstructed) { if ([self performCompletion_step1] == NO) return; [self performCompletion_step2]; [self performCompletion_step3]; [self performCompletion_step4]; } } - (BOOL)performCompletion_step1 { /* Only blindly complete nicknames */ BOOL searchPatternIsEmpty = (self.cachedSearchPattern.length == 0); if (searchPatternIsEmpty && self.isCompletingNickname == NO) { return NO; } /* Define our choices for the completion. The choicesUppercase array holds all the case-sensitive representations of the completions while the choicesLowercase array holds each completion as a lowercase string. The lowercase string is compared against the lowercase backward cut. If they match, then the actual case is requested from choicesUppercase. */ NSMutableArray *choicesUppercase = [NSMutableArray array]; NSMutableArray *choicesLowercase = [NSMutableArray array]; if (self.isCompletingCommand) { for (NSString *command in [IRCCommandIndex localCommandList]) { [choicesUppercase addObject:command.lowercaseString]; } [choicesUppercase addObjectsFromArray:sharedPluginManager().supportedUserInputCommands]; [choicesUppercase addObjectsFromArray:sharedPluginManager().supportedAppleScriptCommands]; /* complete commands in alphabetical order */ [choicesUppercase sortUsingSelector:@selector(localizedCompare:)]; } else if (self.isCompletingChannelName) { IRCClient *client = self.window.selectedClient; IRCChannel *channel = self.window.selectedChannel; if (channel) { [choicesUppercase addObject:channel.name]; } for (IRCChannel *cc in client.channelList) { if (cc == channel) { continue; } [choicesUppercase addObject:cc.name]; } } else if (self.isCompletingNickname) { /* Complete the entire user list. */ IRCClient *client = self.window.selectedClient; IRCChannel *channel = self.window.selectedChannel; if (channel == nil) { return NO; // Umm, where to get channels?... } /* When the search pattern is empty, then special consideration is taken for how the human brain may expect the result. When there is a search pattern, the list is sorted using member weight, but that information is not really relevant when you are targeting all. When the search pattern is empty, the member list is sorted alphabetically and only the single most highly weighted user is placed at the top of the list and that only occurs if there is a user with a different weight. */ __block IRCChannelUser *userWithGreatestWeight = nil; __block BOOL noUserHadGreaterWeightThanOriginal = YES; NSArray *memberList = nil; if (searchPatternIsEmpty == NO) { memberList = [channel.memberList sortedArrayUsingSelector:@selector(compareUsingWeights:)]; userWithGreatestWeight = memberList.firstObject; } else { memberList = [channel.memberList sortedArrayUsingComparator:^NSComparisonResult(IRCChannelUser *user1, IRCChannelUser *user2) { if (userWithGreatestWeight == nil) { userWithGreatestWeight = user1; } if (userWithGreatestWeight.totalWeight < user2.totalWeight) { userWithGreatestWeight = user2; noUserHadGreaterWeightThanOriginal = NO; } return [user1.user.nickname caseInsensitiveCompare:user2.user.nickname]; }]; } /* Add nicknames to list */ NSCharacterSet *nonAlphaCharacters = [NSCharacterSet characterSetWithCharactersInString:@"^[]-_`{}\\"]; void (^addNickname)(NSString *, BOOL) = ^(NSString *nickname, BOOL addTrimmedVariant) { /* Add unmodified version of nickname */ [choicesUppercase addObject:nickname]; [choicesLowercase addObject:nickname.lowercaseString]; /* Add choice after it has been trimmed of special characters as well. */ if (addTrimmedVariant == NO) return; NSString *nicknameTrimmed = [self trimNickname:nickname usingCharacterSet:nonAlphaCharacters]; if (nicknameTrimmed.length > 0 && [nickname isNotEqualTo:nicknameTrimmed]) { NSString *nicknameTrimmedLowercase = nicknameTrimmed.lowercaseString; if ([choicesLowercase containsObject:nicknameTrimmedLowercase] == NO) { [choicesUppercase addObject:nickname]; [choicesLowercase addObject:nicknameTrimmedLowercase]; } } }; BOOL includeTrimmedNicknames = (searchPatternIsEmpty == NO); if (noUserHadGreaterWeightThanOriginal == NO) { addNickname(userWithGreatestWeight.user.nickname, includeTrimmedNicknames); } for (IRCChannelUser *m in memberList) { if (noUserHadGreaterWeightThanOriginal == NO && m == userWithGreatestWeight) { continue; } addNickname(m.user.nickname, includeTrimmedNicknames); } /* Complete static names, including application name. */ addNickname(@"NickServ", NO); addNickname(@"RootServ", NO); addNickname(@"OperServ", NO); addNickname(@"HostServ", NO); addNickname(@"ChanServ", NO); addNickname(@"MemoServ", NO); addNickname([TPCApplicationInfo applicationNameWithoutVersion], NO); /* Complete network name. */ NSString *networkName = client.supportInfo.networkName; if (networkName) { addNickname(networkName, NO); } } /* Quick method for replacing the value of each array object based on a provided selector. */ if (self.isCompletingChannelName || self.isCompletingCommand) { [choicesLowercase addObjectsFromArray:choicesUppercase]; [choicesLowercase performSelectorOnObjectValueAndReplace:@selector(lowercaseString)]; } /* Now that we know the possible matches, we find all values that has our search pattern as its prefix. */ NSMutableArray *choicesLowercaseMatched = nil; NSMutableArray *choicesUppercaseMatched = nil; if (searchPatternIsEmpty) { choicesLowercaseMatched = choicesLowercase; choicesUppercaseMatched = choicesUppercase; } else { choicesUppercaseMatched = [NSMutableArray array]; choicesLowercaseMatched = [NSMutableArray array]; NSString *searchPatternLowercase = self.cachedSearchPattern.lowercaseString; [choicesLowercase enumerateObjectsUsingBlock:^(NSString *choice, NSUInteger choiceIndex, BOOL *stop) { if ([choice hasPrefix:searchPatternLowercase]) { [choicesLowercaseMatched addObject:choice]; [choicesUppercaseMatched addObject:choicesUppercase[choiceIndex]]; } }]; } NSUInteger choicesLowercaseMatchedCount = choicesLowercaseMatched.count; if (choicesLowercaseMatchedCount == 0) { return NO; } /* Now that we know the choices that are actually available to the string being completed; we can filter through each going backwards or forward depending on the call to this method. */ NSString *valueMatchedBySearchPattern = nil; NSUInteger indexOfMatchedValue = self.selectionIndexOfLastCompletion; if (choicesLowercaseMatchedCount <= indexOfMatchedValue || indexOfMatchedValue == NSNotFound) { valueMatchedBySearchPattern = choicesUppercaseMatched[0]; self.selectionIndexOfLastCompletion = 0; } else { if (self.completionIsMovingForward) { indexOfMatchedValue += 1; if (choicesLowercaseMatchedCount <= indexOfMatchedValue) { indexOfMatchedValue = 0; } } else { if (indexOfMatchedValue == 0) { indexOfMatchedValue = (choicesLowercaseMatchedCount - 1); } else { indexOfMatchedValue -= 1; } } valueMatchedBySearchPattern = choicesUppercaseMatched[indexOfMatchedValue]; self.selectionIndexOfLastCompletion = indexOfMatchedValue; } if (self.cachedSearchPatternPrefixCharacter.length > 0) { valueMatchedBySearchPattern = [self.cachedSearchPatternPrefixCharacter stringByAppendingString:valueMatchedBySearchPattern]; } self.completedValue = valueMatchedBySearchPattern; return YES; } - (void)performCompletion_step2 { /* Add the completed string to the spell checker so that a nickname wont show up as spelled incorrectly. The spell checker is cleared of these ignores between channel changes. */ TVCMainWindowTextView *textView = self.window.inputTextField; [RZSpellChecker() ignoreWord:self.completedValue inSpellDocumentWithTag:textView.spellCheckerDocumentTag]; } - (void)performCompletion_step3 { NSString *newCompletionSuffix = nil; BOOL whitespaceAlreadyInPosition = NO; BOOL whitespaceContainedByCachedSuffix = NO; if ([self.cachedCompletionSuffix hasSuffixWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]]) { whitespaceAlreadyInPosition = YES; whitespaceContainedByCachedSuffix = YES; } if (whitespaceAlreadyInPosition == NO) { NSInteger maximumCompletionSuffixEndPoint = (self.currentTextViewStringValue.length - 1); NSInteger nextCharacterInRange = NSMaxRange(self.rangeOfCompletionSuffix); if (nextCharacterInRange < maximumCompletionSuffixEndPoint) { UniChar nextChar = [self.currentTextViewStringValue characterAtIndex:nextCharacterInRange]; if ([[NSCharacterSet whitespaceCharacterSet] characterIsMember:nextChar]) { whitespaceAlreadyInPosition = YES; } } } if (self.isCompletingNickname && self.searchPatternIsAtStart) { NSString *userCompletionSuffix = [TPCPreferences tabCompletionSuffix]; NSInteger userCompletionSuffixLength = userCompletionSuffix.length; if (whitespaceAlreadyInPosition) { if ([userCompletionSuffix hasSuffixWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]]) { if (whitespaceContainedByCachedSuffix == NO && userCompletionSuffixLength > 1) { newCompletionSuffix = [userCompletionSuffix substringToIndex:(userCompletionSuffixLength - 1)]; } else if (whitespaceContainedByCachedSuffix) { newCompletionSuffix = userCompletionSuffix; } } else { newCompletionSuffix = userCompletionSuffix; } } else { if (userCompletionSuffixLength == 0) { BOOL doNotAppendWhitespace = [TPCPreferences tabCompletionDoNotAppendWhitespace]; if (doNotAppendWhitespace == NO) { newCompletionSuffix = @" "; } } else { newCompletionSuffix = userCompletionSuffix; } } } else { if (whitespaceAlreadyInPosition == NO) { newCompletionSuffix = @" "; } } self.completedValueCompletionSuffix = newCompletionSuffix; } - (void)performCompletion_step4 { /* Calculate range of the section that will be replaced. */ NSRange completeReplacementRange; completeReplacementRange.location = self.rangeOfSearchPattern.location; completeReplacementRange.length = (self.rangeOfSearchPattern.length + self.rangeOfCompletionSuffix.length); /* Create the replacement value */ NSString *combinedCompletedValue = nil; if (self.completedValueCompletionSuffix) { combinedCompletedValue = [self.completedValue stringByAppendingString:self.completedValueCompletionSuffix]; } else { combinedCompletedValue = self.completedValue; } /* Perform replacement of selection with the new value */ TVCMainWindowTextView *textView = self.window.inputTextField; if ([textView shouldChangeTextInRange:completeReplacementRange replacementString:combinedCompletedValue]) { [textView replaceCharactersInRange:completeReplacementRange withString:combinedCompletedValue]; [textView didChangeText]; } /* Modify range to account for new length */ completeReplacementRange.length = combinedCompletedValue.length; /* Scroll new selection into view and select it */ NSRange newSelectionRange = NSMakeRange((completeReplacementRange.location + completeReplacementRange.length), 0); [textView scrollRangeToVisible:newSelectionRange]; [textView setSelectedRange:newSelectionRange]; self.selectionRangeAfterLastCompletion = newSelectionRange; /* Calculate range for new suffix */ NSRange completeCompletionSuffixRange; completeCompletionSuffixRange.location = (self.rangeOfSearchPattern.location + self.rangeOfSearchPattern.length); completeCompletionSuffixRange.length = (completeReplacementRange.length - self.rangeOfSearchPattern.length); self.rangeOfCompletionSuffix = completeCompletionSuffixRange; /* Finish operation by updating cache */ self.currentTextViewStringValue = textView.string; self.completedValue = nil; self.completedValueCompletionSuffix = nil; } #pragma mark - #pragma mark Utilities - (NSUInteger)textViewMaximumRange { TVCMainWindowTextView *textView = self.window.inputTextField; return textView.stringLength; } - (nullable NSString *)trimNickname:(NSString *)nickname usingCharacterSet:(NSCharacterSet *)charset { for (NSUInteger i = 0; i < nickname.length; i++) { UniChar c = [nickname characterAtIndex:i]; if ([charset characterIsMember:c]) { continue; } else { return [nickname substringFromIndex:i]; } } return nil; } #pragma mark - #pragma mark Cache Construction - (void)constructCache { [self clearCache]; if ([self constructCachedSearchPattern] == NO) return; if ([self constructCachedSearchPatternPrefixCharacter] == NO) return; if ([self constructCachedCompletionSuffix] == NO) return; self.completionCacheIsConstructed = YES; } - (BOOL)constructCachedSearchPattern { /* Given string and a starting point, we move backwards from that starting point until we reach a comma (,) or a localized space character. */ NSRange selectedRange = self.rangeOfTextSelection; NSInteger searchPatternStartingPoint = 0; if (selectedRange.location > 0) { for (NSInteger i = (selectedRange.location - 1); i >= 0; i--) { UniChar cc = [self.currentTextViewStringValue characterAtIndex:i]; if ([[NSCharacterSet whitespaceCharacterSet] characterIsMember:cc] || cc == '\x02c') { searchPatternStartingPoint = (i + 1); // Starting point is plus one to // position the starting index as // character after separator break; } } } /* Now that we know the starting point, we can extract the search string. */ NSInteger searchPatternLength = (selectedRange.location - searchPatternStartingPoint); self.rangeOfSearchPattern = NSMakeRange(searchPatternStartingPoint, searchPatternLength); if (searchPatternLength == 0) { self.cachedSearchPattern = @""; } else { self.cachedSearchPattern = [self.currentTextViewStringValue substringWithRange:self.rangeOfSearchPattern]; } self.searchPatternIsAtStart = (searchPatternStartingPoint == 0); return YES; } - (BOOL)constructCachedSearchPatternPrefixCharacter { UniChar searchPatternFirstCharacter = 0; // null if (self.cachedSearchPattern.length > 0) { searchPatternFirstCharacter = [self.cachedSearchPattern characterAtIndex:0]; } if (self.searchPatternIsAtStart && searchPatternFirstCharacter == '\x02f') { self.isCompletingCommand = YES; self.cachedSearchPattern = [self.cachedSearchPattern substringFromIndex:1]; self.cachedSearchPatternPrefixCharacter = @"\x02f"; } else if (searchPatternFirstCharacter == '\x040') { self.isCompletingNickname = YES; self.cachedSearchPattern = [self.cachedSearchPattern substringFromIndex:1]; self.cachedSearchPatternPrefixCharacter = @"\x040"; } else if (searchPatternFirstCharacter == '\x023') { self.isCompletingChannelName = YES; } else { self.isCompletingNickname = YES; } if (self.cachedSearchPattern == nil) { return NO; } else { return YES; } } - (BOOL)constructCachedCompletionSuffix { /* Given string and a starting point, we move forward until the user's configured completion prefix is found. If its not found, then we look for a localized space, colon (:), or comma (,) */ NSUInteger totalTextLength = (self.currentTextViewStringValue).length; NSRange selectedRange = self.rangeOfTextSelection; NSUInteger selectedRangeStartPoint = selectedRange.location; NSRange completionSuffixRange; /* If the user has a selection in the text field, then we will be happy to use that as our completion suffix. */ if (selectedRange.length > 0) { completionSuffixRange = selectedRange; goto complete_operation; } else { completionSuffixRange = NSMakeRange(selectedRangeStartPoint, 0); } /* Create search pattern for the user configured completion suffix and search for it. If its found within range, we return that. */ /* If the range from start of our search to the beginning of the user's completion suffix contains a whitespace or exceeds 30 characters, then we do not use that range. */ if (self.isCompletingNickname) { NSString *userCompletionSuffix = [TPCPreferences tabCompletionSuffix]; if (userCompletionSuffix.length > 0) { NSRange completionSearchRange = NSMakeRange(selectedRange.location, (totalTextLength - selectedRange.location)); NSRange completionRangePosition = [self.currentTextViewStringValue rangeOfString:userCompletionSuffix options:0 range:completionSearchRange]; if (NSRangeIsValid(completionRangePosition) && completionRangePosition.length < 30) { NSRange whitespaceSearchRange = NSMakeRange(selectedRange.location, completionRangePosition.location - selectedRange.location); NSRange whitespaceSearchResult = [self.currentTextViewStringValue rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] options:0 range:whitespaceSearchRange]; if (whitespaceSearchResult.location == NSNotFound) { completionSuffixRange.length = (NSMaxRange(completionRangePosition) - selectedRangeStartPoint); goto complete_operation; } } } } /* Search for interesting characters. */ BOOL cutNextWord = [TPCPreferences tabCompletionCutForwardToFirstWhitespace]; if (cutNextWord) { NSInteger maximumCompletionSuffixEndPoint = (totalTextLength - 1); for (NSInteger i = selectedRangeStartPoint; i <= maximumCompletionSuffixEndPoint; i++) { UniChar cc = [self.currentTextViewStringValue characterAtIndex:i]; if ([[NSCharacterSet whitespaceCharacterSet] characterIsMember:cc] || cc == '\x03a' || cc == '\x02c') { completionSuffixRange.length = (i - selectedRangeStartPoint); goto complete_operation; } } // Fallback when we never found a character completionSuffixRange.length = (totalTextLength - selectedRangeStartPoint); } /* Cache relevant information */ complete_operation: self.rangeOfCompletionSuffix = completionSuffixRange; if (self.rangeOfCompletionSuffix.length == 0) { self.cachedCompletionSuffix = @""; } else { self.cachedCompletionSuffix = [self.currentTextViewStringValue substringWithRange:self.rangeOfCompletionSuffix]; } self.searchPatternIsAtEnd = (NSMaxRange(self.rangeOfCompletionSuffix) == totalTextLength); return YES; } #pragma mark - #pragma mark Cache Management - (void)clearCache { self.completedValue = nil; self.completedValueCompletionSuffix = nil; self.cachedSearchPattern = nil; self.cachedSearchPatternPrefixCharacter = nil; self.selectionIndexOfLastCompletion = NSNotFound; self.cachedCompletionSuffix = nil; self.rangeOfCompletionSuffix = NSMakeRange(0, 0); self.rangeOfSearchPattern = NSMakeRange(0, 0); self.completionCacheIsConstructed = NO; self.isCompletingChannelName = NO; self.isCompletingCommand = NO; self.isCompletingNickname = NO; self.searchPatternIsAtEnd = NO; self.searchPatternIsAtStart = NO; } - (void)clear { [self clearCache]; self.currentTextViewStringValue = nil; self.rangeOfTextSelection = NSEmptyRange(); self.selectionRangeAfterLastCompletion = NSEmptyRange(); self.completionIsMovingForward = NO; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLONotificationConfiguration.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TLOLocalization.h" #import "TLONotificationConfigurationPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TXDefaultAlertSoundPreferenceValue = @"Default"; NSString * const TXNoAlertSoundPreferenceValue = @"None"; @interface TLONotificationConfiguration () @property (nonatomic, assign, readwrite) TXNotificationType eventType; @end @implementation TLONotificationConfiguration - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithEventType:(TXNotificationType)aEventType { if ((self = [super init])) { self.eventType = aEventType; return self; } return nil; } + (instancetype)configurationWithEventType:(TXNotificationType)eventType { return [[self alloc] initWithEventType:eventType]; } + (NSString *)localizedAlertDefaultSoundTitle { return TXTLS(@"TVCNotificationConfigurationView[0rs-8l]"); } + (NSString *)localizedAlertNoSoundTitle { return TXTLS(@"TVCNotificationConfigurationView[vje-9n]"); } - (NSString *)displayName { return [sharedNotificationController() titleForEvent:self.eventType]; } - (nullable NSString *)alertSound { [self doesNotRecognizeSelector:_cmd]; return nil; } - (void)setAlertSound:(nullable NSString *)alertSound { [self doesNotRecognizeSelector:_cmd]; } - (NSUInteger)pushNotification { [self doesNotRecognizeSelector:_cmd]; return NSControlStateValueOff; } - (void)setPushNotification:(NSUInteger)pushNotification { [self doesNotRecognizeSelector:_cmd]; } - (NSUInteger)speakEvent { [self doesNotRecognizeSelector:_cmd]; return NSControlStateValueOff; } - (void)setSpeakEvent:(NSUInteger)speakEvent { [self doesNotRecognizeSelector:_cmd]; } - (NSUInteger)disabledWhileAway { [self doesNotRecognizeSelector:_cmd]; return NSControlStateValueOff; } - (void)setDisabledWhileAway:(NSUInteger)disabledWhileAway { [self doesNotRecognizeSelector:_cmd]; } - (NSUInteger)bounceDockIcon { [self doesNotRecognizeSelector:_cmd]; return NSControlStateValueOff; } - (void)setBounceDockIcon:(NSUInteger)bounceDockIcon { [self doesNotRecognizeSelector:_cmd]; } - (NSUInteger)bounceDockIconRepeatedly { [self doesNotRecognizeSelector:_cmd]; return NSControlStateValueOff; } - (void)setBounceDockIconRepeatedly:(NSUInteger)bounceDockIconRepeatedly { [self doesNotRecognizeSelector:_cmd]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLONotificationController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSStringHelper.h" #import "TXMasterController.h" #import "TXMenuController.h" #import "TVCMainWindow.h" #import "TPCApplicationInfo.h" #import "TPCPreferencesLocal.h" #import "TLOLocalization.h" #import "TDCFileTransferDialogPrivate.h" #import "TDCFileTransferDialogTransferControllerPrivate.h" #import "IRCClientPrivate.h" #import "IRCChannel.h" #import "IRCWorld.h" #import "TLONotificationControllerPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TXNotificationUserInfoClientIdentifierKey = @"clientId"; NSString * const TXNotificationUserInfoChannelIdentifierKey = @"channelId"; NSString * const TXNotificationDialogStandardNicknameFormat = @"%@ %@"; NSString * const TXNotificationDialogActionNicknameFormat = @"\u2022 %@: %@"; NSString * const TXNotificationHighlightLogStandardActionFormat = @"\u2022 %@: %@"; NSString * const TXNotificationHighlightLogStandardMessageFormat = @"%@ %@"; NSString * const TXNotificationCategoryIdentifierFileTransfer = @"TXNotificationCategoryIdentifierFileTransfer"; NSString * const TXNotificationActionIdentifierFileTransferAccept = @"TXNotificationActionIdentifierFileTransferAccept"; NSString * const TXNotificationCategoryIdentifierPrivateMessage = @"TXNotificationCategoryIdentifierPrivateMessage"; NSString * const TXNotificationActionIdentifierPrivateMessageReply = @"TXNotificationActionIdentifierPrivateMessageReply"; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 NSString * const TXNotificationCategoryIdentifierLicenseManager = @"TXNotificationCategoryIdentifierLicenseManager"; NSString * const TXNotificationActionIdentifierLicenseManagerMoreInfo = @"TXNotificationActionIdentifierLicenseManagerMoreInfo"; #endif @interface TLONotificationController () @end @implementation TLONotificationController - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return self; } - (void)prepareInitialState { RZUserNotificationCenter().delegate = (id)self; [RZNotificationCenter() addObserver:self selector:@selector(mainWindowSelectionChanged:) name:TVCMainWindowSelectionChangedNotification object:nil]; [RZUserNotificationCenter() requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionProvidesAppNotificationSettings) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (error) { LogToConsoleError("Notifications failed to authorize: %{public}@", error.localizedDescription); } LogToConsoleInfo("Notification permission: %{public}@", StringFromBOOL(granted)); }]; [self registerCategories]; } - (NSSet *)categoriesToRegister { /* File Transfers */ UNNotificationAction *ftAcceptAction = [UNNotificationAction actionWithIdentifier:TXNotificationActionIdentifierFileTransferAccept title:TXTLS(@"Prompts[qpv-go]") options:0]; UNNotificationCategory *ftCategory = [UNNotificationCategory categoryWithIdentifier:TXNotificationCategoryIdentifierFileTransfer actions:@[ftAcceptAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; /* Private Message */ UNTextInputNotificationAction *pmReplyAction = [UNTextInputNotificationAction actionWithIdentifier:TXNotificationActionIdentifierPrivateMessageReply title:TXTLS(@"Notifications[3t4-kl]") options:0 textInputButtonTitle:TXTLS(@"Notifications[bhn-uo]") textInputPlaceholder:TXTLS(@"Notifications[do4-2e]")]; UNNotificationCategory *pmCategory = [UNNotificationCategory categoryWithIdentifier:TXNotificationCategoryIdentifierPrivateMessage actions:@[pmReplyAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 UNNotificationAction *lmMoreInfoAction = [UNNotificationAction actionWithIdentifier:TXNotificationActionIdentifierLicenseManagerMoreInfo title:TXTLS(@"TLOLicenseManager[b8b-sg]") options:0]; UNNotificationCategory *lmCategory = [UNNotificationCategory categoryWithIdentifier:TXNotificationCategoryIdentifierLicenseManager actions:@[lmMoreInfoAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; NSSet *categories = [NSSet setWithObjects:ftCategory, pmCategory, lmCategory, nil]; #else NSSet *categories = [NSSet setWithObjects:ftCategory, pmCategory, nil]; #endif return categories; } - (void)registerCategories { NSSet *categories = [self categoriesToRegister]; [RZUserNotificationCenter() setNotificationCategories:categories]; } - (void)mainWindowSelectionChanged:(NSNotification *)notification { TVCMainWindow *mainWindow = mainWindow(); [self dismissNotificationsForChannel:mainWindow.selectedChannel onClient:mainWindow.selectedClient]; } - (NSString *)titleForEvent:(TXNotificationType)event { #define _df(key, num) case (key): { return TXTLS((num)); } switch (event) { _df(TXNotificationTypeAddressBookMatch, @"Notifications[kx3-xk]") _df(TXNotificationTypeChannelMessage, @"Notifications[qnz-k4]") _df(TXNotificationTypeChannelNotice, @"Notifications[vuq-jp]") _df(TXNotificationTypeConnect, @"Notifications[4lr-ej]") _df(TXNotificationTypeDisconnect, @"Notifications[wjv-yb]") _df(TXNotificationTypeInvite, @"Notifications[eiu-8q]") _df(TXNotificationTypeKick, @"Notifications[2nk-lg]") _df(TXNotificationTypeNewPrivateMessage, @"Notifications[5yi-gu]") _df(TXNotificationTypePrivateMessage, @"Notifications[00b-nx]") _df(TXNotificationTypePrivateNotice, @"Notifications[nhz-io]") _df(TXNotificationTypeHighlight, @"Notifications[cs4-x9]") _df(TXNotificationTypeFileTransferSendSuccessful, @"Notifications[0x2-3h]") _df(TXNotificationTypeFileTransferReceiveSuccessful, @"Notifications[qle-7v]") _df(TXNotificationTypeFileTransferSendFailed, @"Notifications[sc0-1n]") _df(TXNotificationTypeFileTransferReceiveFailed, @"Notifications[we9-1b]") _df(TXNotificationTypeFileTransferReceiveRequested, @"Notifications[st5-0n]") _df(TXNotificationTypeUserJoined, @"Notifications[25q-af]") _df(TXNotificationTypeUserParted, @"Notifications[k3s-by]") _df(TXNotificationTypeUserDisconnected, @"Notifications[0fo-bt]") } #undef _df return nil; } - (void)notify:(TXNotificationType)eventType title:(nullable NSString *)eventTitle description:(nullable NSString *)eventDescription userInfo:(nullable NSDictionary *)eventContext { switch (eventType) { case TXNotificationTypeHighlight: { eventTitle = TXTLS(@"Notifications[qka-f3]", eventTitle); break; } case TXNotificationTypeNewPrivateMessage: { eventTitle = TXTLS(@"Notifications[ltn-hf]"); break; } case TXNotificationTypeChannelMessage: { eventTitle = TXTLS(@"Notifications[ep5-de]", eventTitle); break; } case TXNotificationTypeChannelNotice: { eventTitle = TXTLS(@"Notifications[chi-km]", eventTitle); break; } case TXNotificationTypePrivateMessage: { eventTitle = TXTLS(@"Notifications[69i-dy]"); break; } case TXNotificationTypePrivateNotice: { eventTitle = TXTLS(@"Notifications[7hn-dg]"); break; } case TXNotificationTypeKick: { eventTitle = TXTLS(@"Notifications[u30-ia]", eventTitle); break; } case TXNotificationTypeInvite: { eventTitle = TXTLS(@"Notifications[g4s-cq]", eventTitle); break; } case TXNotificationTypeConnect: { eventTitle = TXTLS(@"Notifications[mo1-vn]", eventTitle); eventDescription = TXTLS(@"Notifications[88k-kl]"); break; } case TXNotificationTypeDisconnect: { eventTitle = TXTLS(@"Notifications[7xe-ig]", eventTitle); eventDescription = TXTLS(@"Notifications[bif-2c]"); break; } case TXNotificationTypeAddressBookMatch: { eventTitle = TXTLS(@"Notifications[niq-32]"); break; } case TXNotificationTypeFileTransferSendSuccessful: { eventTitle = TXTLS(@"Notifications[l5y-sx]", eventTitle); break; } case TXNotificationTypeFileTransferReceiveSuccessful: { eventTitle = TXTLS(@"Notifications[hc9-7n]", eventTitle); break; } case TXNotificationTypeFileTransferSendFailed: { eventTitle = TXTLS(@"Notifications[het-vh]", eventTitle); break; } case TXNotificationTypeFileTransferReceiveFailed: { eventTitle = TXTLS(@"Notifications[hm4-ze]", eventTitle); break; } case TXNotificationTypeFileTransferReceiveRequested: { eventTitle = TXTLS(@"Notifications[nqz-7v]", eventTitle); break; } case TXNotificationTypeUserJoined: { eventTitle = TXTLS(@"Notifications[keq-ts]", eventTitle); break; } case TXNotificationTypeUserParted: { eventTitle = TXTLS(@"Notifications[im4-p0]", eventTitle); break; } case TXNotificationTypeUserDisconnected: { eventTitle = TXTLS(@"Notifications[20x-32]", eventTitle); break; } } if ([TPCPreferences removeAllFormatting] == NO) { eventDescription = eventDescription.stripIRCEffects; } NSString *categoryIdentifier = nil; if (eventType == TXNotificationTypeFileTransferReceiveRequested) { categoryIdentifier = TXNotificationCategoryIdentifierFileTransfer; } else if (eventType == TXNotificationTypeNewPrivateMessage || eventType == TXNotificationTypePrivateMessage) { categoryIdentifier = TXNotificationCategoryIdentifierPrivateMessage; } NSString *clientId = eventContext[TXNotificationUserInfoClientIdentifierKey]; NSString *channelId = eventContext[TXNotificationUserInfoChannelIdentifierKey]; NSString *threadIdentifier = [self threadIdentifierForClient:clientId channel:channelId]; [self scheduleNotificationWithTitle:eventTitle message:eventDescription userInfo:eventContext notificationIdentifier:nil threadIdentifier:threadIdentifier categoryIdentifier:categoryIdentifier]; } - (nullable NSString *)threadIdentifierForClient:(nullable NSString *)clientIdentifier channel:(nullable NSString *)channelIdentifier { if (clientIdentifier == nil) { return nil; } if (channelIdentifier) { return [clientIdentifier stringByAppendingFormat:@"-%@", channelIdentifier]; } return clientIdentifier; } - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message userInfo:(nullable NSDictionary *)userInfo { NSParameterAssert(title != nil); NSParameterAssert(message != nil); [self scheduleNotificationWithTitle:title message:message userInfo:userInfo notificationIdentifier:nil threadIdentifier:nil categoryIdentifier:nil]; } - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message userInfo:(nullable NSDictionary *)userInfo threadIdentifier:(NSString *)threadIdentifier { NSParameterAssert(title != nil); NSParameterAssert(message != nil); NSParameterAssert(threadIdentifier != nil); [self scheduleNotificationWithTitle:title message:message userInfo:userInfo notificationIdentifier:nil threadIdentifier:threadIdentifier categoryIdentifier:nil]; } - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message forChannel:(IRCChannel *)channel { NSParameterAssert(title != nil); NSParameterAssert(message != nil); NSParameterAssert(channel != nil); IRCClient *client = channel.associatedClient; [self scheduleNotificationWithTitle:title message:message forChannel:channel onClient:client]; } - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message onClient:(IRCClient *)client { NSParameterAssert(title != nil); NSParameterAssert(message != nil); NSParameterAssert(client != nil); [self scheduleNotificationWithTitle:title message:message forChannel:nil onClient:client]; } - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message forChannel:(nullable IRCChannel *)channel onClient:(IRCClient *)client { NSParameterAssert(title != nil); NSParameterAssert(message != nil); NSParameterAssert(client != nil); NSString *clientId = client.uniqueIdentifier; NSString *channelId = channel.uniqueIdentifier; NSString *threadIdentifier = [self threadIdentifierForClient:clientId channel:channelId]; NSDictionary *userInfo = nil; if (channelId) { userInfo = @{TXNotificationUserInfoClientIdentifierKey : clientId, TXNotificationUserInfoChannelIdentifierKey : channelId}; } else { userInfo = @{TXNotificationUserInfoClientIdentifierKey : clientId}; } [self scheduleNotificationWithTitle:title message:message userInfo:userInfo notificationIdentifier:nil threadIdentifier:threadIdentifier categoryIdentifier:nil]; } - (void)scheduleNotificationWithTitle:(NSString *)title message:(NSString *)message userInfo:(nullable NSDictionary *)userInfo notificationIdentifier:(nullable NSString *)notificationIdentifier threadIdentifier:(nullable NSString *)threadIdentifier categoryIdentifier:(nullable NSString *)categoryIdentifier { NSParameterAssert(title != nil); NSParameterAssert(message != nil); UNMutableNotificationContent *notificationContent = [UNMutableNotificationContent new]; notificationContent.title = title; notificationContent.body = message; if (userInfo) { notificationContent.userInfo = userInfo; } if (categoryIdentifier) { notificationContent.categoryIdentifier = categoryIdentifier; } if (threadIdentifier) { notificationContent.threadIdentifier = threadIdentifier; } /* The notification identifier should be unique to the specific notification because otherwise the system will replace existing notifications of the same identifier. That's not a bad behavior. Just not one we want. */ /* Textual will format the identifier as such: TXNotification[-[-]]-- */ if (notificationIdentifier == nil) { notificationIdentifier = [NSString stringWithFormat:@"TXNotification-%@-%ld-%ld", ((threadIdentifier) ?: @""), title.hash, message.hash];; } [self scheduleNotificationWithContent:notificationContent identifier:notificationIdentifier]; } - (void)scheduleNotificationWithContent:(UNNotificationContent *)notificationContent identifier:(nullable NSString *)notificationIdentifier { NSParameterAssert(notificationContent != nil); NSParameterAssert(notificationIdentifier != nil); UNNotificationRequest *notificationRequest = [UNNotificationRequest requestWithIdentifier:notificationIdentifier content:notificationContent trigger:nil]; [self scheduleNotificationRequest:notificationRequest]; } - (void)scheduleNotificationRequest:(UNNotificationRequest *)request { NSParameterAssert(request != nil); [RZUserNotificationCenter() addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (error) { LogToConsoleError("Failed to post notification '%{private}@': %{public}@", request.content.title, error.localizedDescription); } }]; } #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 - (void)scheduleLicenseManagerNotificationWithTitle:(NSString *)title message:(NSString *)message { NSParameterAssert(title != nil); NSParameterAssert(message != nil); [self scheduleNotificationWithTitle:title message:message userInfo:nil notificationIdentifier:nil threadIdentifier:nil categoryIdentifier:TXNotificationCategoryIdentifierLicenseManager]; } #endif #pragma mark - #pragma mark Notification Center Delegate - (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(nullable UNNotification *)notification { [menuController() showNotificationPreferences:nil]; } - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner); } - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { NSString *message = nil; if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) { message = [((UNTextInputNotificationResponse *)response) userText]; } /* Now that is what you call chaining... */ NSDictionary *userInfo = response.notification.request.content.userInfo; [self notificationResponseReceived:response context:userInfo withReplyMessage:message]; } - (void)dismissNotificationsForChannel:(nullable IRCChannel *)channel onClient:(IRCClient *)client { NSParameterAssert(client != nil); NSString *clientId = client.uniqueIdentifier; NSString *channelId = channel.uniqueIdentifier; LogToConsoleDebug("Dismissing notifications for '%{public}@' on '%{public}@'", ((channelId) ?: @""), clientId); /* Pending Notifications */ [RZUserNotificationCenter() getPendingNotificationRequestsWithCompletionHandler:^(NSArray *requests) { NSMutableArray *notificationIdentifiers = [NSMutableArray array]; [requests enumerateObjectsUsingBlock:^(UNNotificationRequest *request, NSUInteger index, BOOL *stop) { if ([self isNotificationRequest:request inScopeOfChannel:channel onClient:client]) { [notificationIdentifiers addObject:request.identifier]; } }]; if (notificationIdentifiers.count == 0) { return; } [RZUserNotificationCenter() removePendingNotificationRequestsWithIdentifiers:notificationIdentifiers]; LogToConsoleDebug("Dismissed %{public}ld pending notifications", notificationIdentifiers.count); }]; /* Delivered Notifications */ [RZUserNotificationCenter() getDeliveredNotificationsWithCompletionHandler:^(NSArray *notifications) { NSMutableArray *notificationIdentifiers = [NSMutableArray array]; [notifications enumerateObjectsUsingBlock:^(UNNotification *notification, NSUInteger index, BOOL *stop) { UNNotificationRequest *request = notification.request; if ([self isNotificationRequest:request inScopeOfChannel:channel onClient:client]) { [notificationIdentifiers addObject:request.identifier]; } }]; if (notificationIdentifiers.count == 0) { return; } [RZUserNotificationCenter() removeDeliveredNotificationsWithIdentifiers:notificationIdentifiers]; LogToConsoleDebug("Dismissed %{public}ld delivered notifications", notificationIdentifiers.count); }]; } - (BOOL)isNotificationRequest:(UNNotificationRequest *)request inScopeOfChannel:(nullable IRCChannel *)channel onClient:(IRCClient *)client { NSParameterAssert(request != nil); NSParameterAssert(client != nil); NSString *clientIdLeft = client.uniqueIdentifier; NSString *channelIdLeft = channel.uniqueIdentifier; NSDictionary *userInfo = request.content.userInfo; NSString *clientIdRight = userInfo[TXNotificationUserInfoClientIdentifierKey]; NSString *channelIdRight = userInfo[TXNotificationUserInfoChannelIdentifierKey]; /* NSObjectsAreEqual() checks for equality of nil so it is valid if channel ID left and right are both nil. */ return (NSObjectsAreEqual(clientIdLeft, clientIdRight) && NSObjectsAreEqual(channelIdLeft, channelIdRight)); } #pragma mark - #pragma mark Notification Callback - (void)notificationResponseReceived:(UNNotificationResponse *)response context:(NSDictionary *)context withReplyMessage:(nullable NSString *)message { NSParameterAssert(context != nil); NSString *actionIdentifier = [response actionIdentifier]; if ([actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) { LogToConsoleDebug("Dismissed notification: '%{private}@'", response); return; } /* If we ever expand beyond a few different actions, then revisit this so that we aren't just declaring a bunch of booleans. This was just the easier solution at the time. */ BOOL isFileTransferAction = [actionIdentifier isEqualToString:TXNotificationActionIdentifierFileTransferAccept]; BOOL isPrivateMessageAction = [actionIdentifier isEqualToString:TXNotificationActionIdentifierPrivateMessageReply]; BOOL isLicenseManagerAction = NO; #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 if ([actionIdentifier isEqualToString:TXNotificationActionIdentifierLicenseManagerMoreInfo]) { isLicenseManagerAction = YES; } #endif BOOL activateApp = (isPrivateMessageAction == NO); BOOL keyMainWindow = (isPrivateMessageAction == NO && isFileTransferAction == NO && isLicenseManagerAction == NO); if (activateApp) { [NSApp activateIgnoringOtherApps:YES]; } if (keyMainWindow) { [mainWindow() makeKeyAndOrderFront:nil]; } #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 /* Handle a notification that was clicked related to a warnings about the trial of Textual preparing to expire. */ if (isLicenseManagerAction) { [menuController() manageLicense:nil]; } else #endif /* Handle file transfer notifications allowing the user to start a file transfer directly through the notification's action button. */ if (isFileTransferAction) { [[TXSharedApplication sharedFileTransferDialog] show:YES restorePosition:NO]; NSInteger alertType = [context integerForKey:@"fileTransferNotificationType"]; if (alertType != TXNotificationTypeFileTransferReceiveRequested) { return; } NSString *uniqueIdentifier = context[@"fileTransferUniqueIdentifier"]; TDCFileTransferDialogTransferController *fileTransfer = [[TXSharedApplication sharedFileTransferDialog] fileTransferWithUniqueIdentifier:uniqueIdentifier]; if (fileTransfer == nil) { return; } TDCFileTransferDialogTransferStatus transferStatus = fileTransfer.transferStatus; if (transferStatus != TDCFileTransferDialogTransferStatusStopped) { return; } [fileTransfer openWithPathOrUserDownloads]; } /* Handle all other IRC related notifications. */ else { NSString *clientId = context[TXNotificationUserInfoClientIdentifierKey]; NSString *channelId = context[TXNotificationUserInfoChannelIdentifierKey]; if (clientId == nil) { return; } IRCClient *client = nil; IRCChannel *channel = nil; if (channelId) { channel = [worldController() findChannelWithId:channelId onClientWithId:clientId]; } else { client = [worldController() findClientWithId:clientId]; } if (channel) { [mainWindow() select:channel]; } else if (client) { [mainWindow() select:client]; } if (channel == nil) { return; } if (message.length == 0) { return; } [channel.associatedClient inputText:message destination:channel]; } } @end #pragma mark - @implementation TLONotificationController (Preferences) - (nullable NSString *)soundForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel { if (channel) { NSString *channelValue = [channel.config soundForEvent:event]; if (channelValue != nil) { return channelValue; } } return [TPCPreferences soundForEvent:event]; } - (BOOL)speakEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel { if (channel) { NSUInteger channelValue = [channel.config speakEvent:event]; if (channelValue != NSControlStateValueMixed) { return (channelValue == NSControlStateValueOn); } } return [TPCPreferences speakEvent:event]; } - (BOOL)notificationEnabledForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel { if (channel) { NSUInteger channelValue = [channel.config notificationEnabledForEvent:event]; if (channelValue != NSControlStateValueMixed) { return (channelValue == NSControlStateValueOn); } } return [TPCPreferences notificationEnabledForEvent:event]; } - (BOOL)disabledWhileAwayForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel { if (channel) { NSUInteger channelValue = [channel.config disabledWhileAwayForEvent:event]; if (channelValue != NSControlStateValueMixed) { return (channelValue == NSControlStateValueOn); } } return [TPCPreferences disabledWhileAwayForEvent:event]; } - (BOOL)bounceDockIconForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel { if (channel) { NSUInteger channelValue = [channel.config bounceDockIconForEvent:event]; if (channelValue != NSControlStateValueMixed) { return (channelValue == NSControlStateValueOn); } } return [TPCPreferences bounceDockIconForEvent:event]; } - (BOOL)bounceDockIconRepeatedlyForEvent:(TXNotificationType)event inChannel:(nullable IRCChannel *)channel { if (channel) { NSUInteger channelValue = [channel.config bounceDockIconRepeatedlyForEvent:event]; if (channelValue != NSControlStateValueMixed) { return (channelValue == NSControlStateValueOn); } } return [TPCPreferences bounceDockIconRepeatedlyForEvent:event]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOSoundPlayer.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "TLONotificationConfigurationPrivate.h" #import "TLOSoundPlayer.h" NS_ASSUME_NONNULL_BEGIN @implementation TLOSoundPlayer + (NSDictionary *)soundFilesAtPath:(NSString *)path { NSMutableDictionary *resultData = [NSMutableDictionary dictionary]; NSArray *files = [RZFileManager() contentsOfDirectoryAtPath:path error:NULL]; for (NSString *file in files) { NSString *filePath = [path stringByAppendingPathComponent:file]; NSString *fileWithoutExtension = file.stringByDeletingPathExtension; resultData[fileWithoutExtension] = filePath; } return resultData; } + (nullable NSDictionary *)systemAlertSoundFiles { NSArray *folders = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSSystemDomainMask, YES); if (folders.count == 0) { return nil; } NSString *folder = [folders[0] stringByAppendingPathComponent:@"/Sounds/"]; return [self soundFilesAtPath:folder]; } + (nullable NSDictionary *)systemLibrarySoundFiles { NSArray *folders = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSLocalDomainMask, YES); if (folders.count == 0) { return nil; } NSString *folder = [folders[0] stringByAppendingPathComponent:@"/Sounds/"]; return [self soundFilesAtPath:folder]; } + (nullable NSDictionary *)userLibrarySoundFiles { NSArray *folders = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); if (folders.count == 0) { return nil; } NSString *folder = [folders[0] stringByAppendingPathComponent:@"/Sounds/"]; return [self soundFilesAtPath:folder]; } + (void)doesSoundFileDictionary:(NSDictionary *)fileList containName:(NSString *)name returnedPath:(NSString **)path { NSString *filePath = fileList[name]; if (filePath == nil) { return; } NSString *fileExtension = filePath.pathExtension; CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag( kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL); if (UTTypeConformsTo(fileUTI, kUTTypeAudio) == false) { LogToConsoleDebug("File is not audio file: '%{public}@'", filePath.standardizedTildePath); CFRelease(fileUTI); return; } CFRelease(fileUTI); if ( path) { *path = filePath; } } + (SystemSoundID)alertSoundNamed:(NSString *)name { NSString *soundPath = nil; NSDictionary *soundFiles = [self userLibrarySoundFiles]; [self doesSoundFileDictionary:soundFiles containName:name returnedPath:&soundPath]; if (soundPath == nil) { soundFiles = [self systemLibrarySoundFiles]; [self doesSoundFileDictionary:soundFiles containName:name returnedPath:&soundPath]; } if (soundPath == nil) { soundFiles = [self systemAlertSoundFiles]; [self doesSoundFileDictionary:soundFiles containName:name returnedPath:&soundPath]; } if (soundPath == nil) { return 0; } NSURL *soundPathURL = [NSURL fileURLWithPath:soundPath]; SystemSoundID soundID; OSStatus soundLoadError = AudioServicesCreateSystemSoundID((__bridge CFURLRef)(soundPathURL), &soundID); if (soundLoadError == noErr) { return soundID; } LogToConsoleError("Returned error code %{public}i when loading file at path: %{public}@", soundLoadError, soundPathURL.standardizedTildePath); return 0; } + (void)playAlertSound:(NSString *)name { NSParameterAssert(name != nil); if ([name isEqualToString:TXNoAlertSoundPreferenceValue]) { return; } if ([name isEqualToString:@"Beep"]) { NSBeep(); return; } SystemSoundID soundID = [self alertSoundNamed:name]; if (soundID) { AudioServicesPlayAlertSound(soundID); // AudioServicesDisposeSystemSoundID(soundID); } else { LogToConsoleError("Error: Unable to locate sound '%{public}@'", name); } } + (NSArray *)uniqueListOfSounds { NSMutableArray *sounds = [NSMutableArray array]; [sounds addObject:@"Beep"]; // For NSBeep() for (NSString *sound in [self systemAlertSoundFiles]) { if ([sounds containsObject:sound] == NO) { [sounds addObject:sound]; } } for (NSString *sound in [self systemLibrarySoundFiles]) { if ([sounds containsObject:sound] == NO) { [sounds addObject:sound]; } } for (NSString *sound in [self userLibrarySoundFiles]) { if ([sounds containsObject:sound] == NO) { [sounds addObject:sound]; } } [sounds sortUsingSelector:@selector(caseInsensitiveCompare:)]; return [sounds copy]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOSpeechSynthesizer.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClientPrivate.h" #import "TLOSpokenNotificationPrivate.h" #import "TLOSpeechSynthesizerPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TLOSpeechSynthesizer () @property (nonatomic, strong) NSSpeechSynthesizer *speechSynthesizer; @property (nonatomic, strong) NSMutableArray *itemsToBeSpoken; @property (nonatomic, assign) BOOL isWaitingForSystemToStopSpeaking; @end @implementation TLOSpeechSynthesizer - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.itemsToBeSpoken = [NSMutableArray array]; self.speechSynthesizer = [NSSpeechSynthesizer new]; self.speechSynthesizer.delegate = (id)self; self.isStopped = NO; } - (void)dealloc { self.speechSynthesizer.delegate = nil; } #pragma mark - #pragma mark Public API - (void)speak:(id)object { NSParameterAssert(object != nil); if (self.isStopped) { return; } @synchronized(self.itemsToBeSpoken) { [self.itemsToBeSpoken addObject:object]; } if (self.isSpeaking == NO) { [self speakNextItem]; } } - (void)speakNextItemWhenSystemFinishes { /* This method sleeps the thread for one second each pass then to check if another application on the system is speaking. */ while ([NSSpeechSynthesizer isAnyApplicationSpeaking]) { [NSThread sleepForTimeInterval:1.0]; } self.isWaitingForSystemToStopSpeaking = NO; [self speakNextItem]; } - (void)speakNextItem { if (self.isStopped) { return; } @synchronized(self.itemsToBeSpoken) { id nextMessage = self.itemsToBeSpoken.firstObject; if (nextMessage == nil) { return; } if ([NSSpeechSynthesizer isAnyApplicationSpeaking]) { if (self.isWaitingForSystemToStopSpeaking == NO) { self.isWaitingForSystemToStopSpeaking = YES; XRPerformBlockAsynchronouslyOnGlobalQueueWithPriority(^{ [self speakNextItemWhenSystemFinishes]; }, DISPATCH_QUEUE_PRIORITY_LOW); } return; } [self.itemsToBeSpoken removeObjectAtIndex:0]; if ([nextMessage isKindOfClass:[TLOSpokenNotification class]]) { nextMessage = [(IRCClient *)[nextMessage client] formatNotificationToSpeak:nextMessage]; // Returning nil does not throw an assert so that the client can chose // to reject specific events for whatever reason it wants. if (nextMessage == nil) { [self speakNextItem]; return; } } [self.speechSynthesizer startSpeakingString:nextMessage]; } } - (void)stopSpeakingAndMoveForward { if (self.isSpeaking == NO) { return; } [self.speechSynthesizer stopSpeaking]; // Will call delegate to do next item } - (void)stopSpeakingIfSet { if (self.isSpeaking == NO || self.isStopped == NO) { return; } [self.speechSynthesizer stopSpeaking]; } - (BOOL)isSpeaking { return self.speechSynthesizer.isSpeaking; } - (void)setIsStopped:(BOOL)isStopped { if (self->_isStopped != isStopped) { self->_isStopped = isStopped; [self stopSpeakingIfSet]; } } - (void)clearQueue { @synchronized(self.itemsToBeSpoken) { [self.itemsToBeSpoken removeAllObjects]; } } - (void)clearQueueForClient:(IRCClient *)client { @synchronized(self.itemsToBeSpoken) { NSIndexSet *indexesToRemove = [self.itemsToBeSpoken indexesOfObjectsPassingTest:^BOOL(id object, NSUInteger index, BOOL *stop) { if ([object isKindOfClass:[TLOSpokenNotification class]]) { return ((IRCClient *)[object client] == client); } return NO; }]; if (indexesToRemove.count == 0) { return; } [self.itemsToBeSpoken removeObjectsAtIndexes:indexesToRemove]; } } #pragma mark - #pragma mark Delegate Callback - (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)finishedSpeaking { [self speakNextItem]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOSpokenNotification.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClient.h" #import "IRCChannel.h" #import "TLOSpokenNotificationPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TLOSpokenNotification () @property (nonatomic, weak, null_unspecified, readwrite) IRCClient *client; @property (nonatomic, weak, null_unspecified, readwrite) IRCChannel *channel; @property (nonatomic, copy, null_unspecified, readwrite) NSString *nickname; @property (nonatomic, copy, null_unspecified, readwrite) NSString *text; @property (nonatomic, readwrite) TVCLogLineType lineType; @property (nonatomic, readwrite) TXNotificationType notificationType; @end @implementation TLOSpokenNotification - (instancetype)initWithNotification:(TXNotificationType)notificationType lineType:(TVCLogLineType)lineType target:(null_unspecified IRCTreeItem *)target nickname:(null_unspecified NSString *)nickname text:(null_unspecified NSString *)text { if ((self = [super init])) { self.notificationType = notificationType; self.lineType = lineType; if (target.isClient) { self.client = (IRCClient *)target; } else { self.client = (IRCClient *)target.associatedClient; self.channel = (IRCChannel *)target; } self.nickname = nickname; self.text = text; return self; } return nil; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Library/TLOpenLink.swift ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ @objc(TLOpenLink) public class OpenLink: NSObject { @objc(open:inBackground:) public static func open(url: URL, inBackground: Bool = TPCPreferences.openBrowserInBackground()) { if inBackground { /* User should not be clicking links frequently enough that we need to worry about making the configuration static. */ let configuration = NSWorkspace.OpenConfiguration(); configuration.activates = false NSWorkspace.shared.open(url, configuration: configuration) return } NSWorkspace.shared.open(url) } @objc(openWithString:inBackground:) public static func open(string: String, inBackground: Bool = TPCPreferences.openBrowserInBackground()) { guard let urlToOpen = URL(string: string) else { return } open(url: urlToOpen, inBackground: inBackground) } } extension OpenLink { @objc(open:) public static func openBridged(url: URL) { open(url: url) } @objc(openWithString:) public static func openBridged(string: String) { open(string: string) } } ================================================ FILE: Sources/App/Classes/Others/main.m ================================================ #import "TXApplicationPrivate.h" int main(int argc, const char *argv[]) { @autoreleasepool { #ifndef DEBUG if ([TXApplication checkForOtherCopiesOfTextualRunning] == NO) { exit(0); } #endif NSApplicationMain(argc, argv); } return 0; } ================================================ FILE: Sources/App/Classes/Preferences/TPCApplicationInfo.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "BuildConfig.h" #import "TPCPreferencesUserDefaults.h" #import "TPCApplicationInfoPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TPCApplicationInfo + (NSString *)applicationName { return TXBundleBuildProductName; } + (NSString *)applicationNameWithoutVersion { NSString *applicationName = TXBundleBuildProductName; NSInteger spacePosition = [applicationName stringPosition:@" "]; if (spacePosition > 0) { return [applicationName substringToIndex:spacePosition]; } else { return applicationName; } } + (NSString *)applicationVersion { return TXBundleBuildVersion; } + (NSString *)applicationVersionShort { return TXBundleBuildVersionShort; } + (int)applicationProcessID { return RZProcessInfo().processIdentifier; } + (NSString *)applicationBundleIdentifier { return TXBundleBuildProductIdentifier; } + (NSString *)applicationBuildScheme { return TXBundleBuildScheme; } + (NSDictionary *)applicationInfoPlist { return RZMainBundle().infoDictionary; } + (BOOL)sandboxEnabled { #if TEXTUAL_BUILT_INSIDE_SANDBOX == 1 return YES; #else return NO; #endif } + (nullable NSDate *)applicationLaunchDate { NSRunningApplication *runningApp = [NSRunningApplication currentApplication]; return runningApp.launchDate; } + (NSTimeInterval)timeIntervalSinceApplicationLaunch { NSDate *launchDate = [self applicationLaunchDate]; if (launchDate == nil) { return 0; } return (launchDate.timeIntervalSinceNow * (-1)); } + (NSTimeInterval)timeIntervalSinceApplicationInstall { NSTimeInterval runTime = [self timeIntervalSinceApplicationLaunch]; NSTimeInterval runTimeTotal = [RZUserDefaults() doubleForKey:@"TXRunTime"]; return (runTimeTotal + runTime); } + (void)saveTimeIntervalSinceApplicationInstall { NSTimeInterval timeInterval = [self timeIntervalSinceApplicationInstall]; [RZUserDefaults() setDouble:timeInterval forKey:@"TXRunTime"]; } + (NSUInteger)applicationRunCount { return [RZUserDefaults() unsignedIntegerForKey:@"TXRunCount"]; } + (void)incrementApplicationRunCount { NSUInteger runCount = ([self applicationRunCount] + 1); [RZUserDefaults() setUnsignedInteger:runCount forKey:@"TXRunCount"]; } + (NSTimeInterval)applicationBirthday { /* The reference date is the date & time of the first commit to the Textual repo. Textual existed before then, of course, but the date will remain as the official reference date for its birthday. */ /* The date decodes to July 23, 2010 03:53:00 AM */ return 1279871580.000000; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/TPCPathInfo.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "BuildConfig.h" #import "TDCAlert.h" #import "TLOLocalization.h" #import "TPCPreferencesLocal.h" #import "TPCPreferencesUserDefaults.h" #import "TPCPathInfoPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TPCPathInfo #pragma mark - #pragma mark Utilities + (void)_createDirectoryAtPath:(NSString *)directoryPath { NSParameterAssert(directoryPath != nil); if ([RZFileManager() fileExistsAtPath:directoryPath]) { return; } NSError *createDirectoryError = nil; if ([RZFileManager() createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&createDirectoryError] == NO) { LogToConsoleError("Failed to create directory at path: '%{public}@' - %{public}@", directoryPath.standardizedTildePath, createDirectoryError.localizedDescription); } } + (void)_createDirectoryAtURL:(NSURL *)directoryURL { NSParameterAssert(directoryURL != nil); if ([RZFileManager() fileExistsAtURL:directoryURL]) { return; } NSError *createDirectoryError = nil; if ([RZFileManager() createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:nil error:&createDirectoryError] == NO) { LogToConsoleError("Failed to create directory at path: '%{public}@' - %{public}@", directoryURL.standardizedTildePath, createDirectoryError.localizedDescription); } } #pragma mark - #pragma mark Application Specific + (NSString *)applicationBundle { return RZMainBundle().bundlePath; } + (NSURL *)applicationBundleURL { return RZMainBundle().bundleURL; } + (NSString *)applicationResources { return RZMainBundle().resourcePath; } + (NSURL *)applicationResourcesURL { return RZMainBundle().resourceURL; } + (nullable NSString *)applicationCaches { NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); if (pathArray.count == 0) { return nil; } NSString *endPath = [NSString stringWithFormat:@"/%@/", TXBundleBuildProductIdentifier]; NSString *basePath = [pathArray.firstObject stringByAppendingPathComponent:endPath]; [self _createDirectoryAtPath:basePath]; return basePath; } + (nullable NSURL *)applicationCachesURL { NSString *sourcePath = self.applicationCaches; if (sourcePath == nil) { return nil; } return [NSURL fileURLWithPath:sourcePath isDirectory:YES]; } + (nullable NSString *)groupContainer { NSURL *sourceURL = self.groupContainerURL; if (sourceURL == nil) { return nil; } return sourceURL.path; } + (nullable NSURL *)groupContainerURL { NSURL *baseURL = [RZFileManager() containerURLForSecurityApplicationGroupIdentifier:TXBundleBuildGroupContainerIdentifier]; return baseURL; } + (nullable NSString *)groupContainerApplicationCaches { NSURL *sourceURL = self.groupContainerApplicationCachesURL; if (sourceURL == nil) { return nil; } return sourceURL.path; } + (nullable NSURL *)groupContainerApplicationCachesURL { NSURL *sourceURL = self.groupContainerURL; if (sourceURL == nil) { return nil; } NSURL *baseURL = [sourceURL URLByAppendingPathComponent:@"/Library/Caches/"]; [self _createDirectoryAtURL:baseURL]; return baseURL; } + (nullable NSString *)applicationSupport { NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); if (pathArray.count == 0) { return nil; } NSString *basePath = [pathArray.firstObject stringByAppendingPathComponent:@"/Textual/"]; [self _createDirectoryAtPath:basePath]; return basePath; } + (nullable NSURL *)applicationSupportURL { NSString *sourcePath = self.applicationSupport; if (sourcePath == nil) { return nil; } return [NSURL fileURLWithPath:sourcePath isDirectory:YES]; } + (nullable NSString *)groupContainerApplicationSupport { NSURL *sourceURL = self.groupContainerApplicationSupportURL; if (sourceURL == nil) { return nil; } return sourceURL.path; } + (nullable NSURL *)groupContainerApplicationSupportURL { NSURL *sourceURL = self.groupContainerURL; if (sourceURL == nil) { return nil; } NSURL *baseURL = [sourceURL URLByAppendingPathComponent:@"/Library/Application Support/Textual/"]; [self _createDirectoryAtURL:baseURL]; return baseURL; } + (nullable NSString *)applicationLogs { NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); if (pathArray.count == 0) { return nil; } NSString *endPath = [NSString stringWithFormat:@"/Logs/%@/", TXBundleBuildProductIdentifier]; NSString *basePath = [pathArray.firstObject stringByAppendingPathComponent:endPath]; [self _createDirectoryAtPath:basePath]; return basePath; } + (nullable NSURL *)applicationLogsURL { NSString *sourcePath = self.applicationLogs; if (sourcePath == nil) { return nil; } return [NSURL fileURLWithPath:sourcePath isDirectory:YES]; } + (NSString *)applicationTemporary { NSString *sourcePath = NSTemporaryDirectory(); NSString *endPath = [NSString stringWithFormat:@"/%@/", TXBundleBuildProductIdentifier]; NSString *basePath = [sourcePath stringByAppendingPathComponent:endPath]; [self _createDirectoryAtPath:basePath]; return basePath; } + (NSURL *)applicationTemporaryURL { return [NSURL fileURLWithPath:self.applicationTemporary isDirectory:YES]; } + (NSString *)applicationTemporaryProcessSpecific { NSString *sourcePath = self.applicationTemporary; int processIdentifier = [[NSProcessInfo processInfo] processIdentifier]; NSString *endPath = [NSString stringWithFormat:@"/tmp-%i", processIdentifier]; NSString *basePath = [sourcePath stringByAppendingPathComponent:endPath]; [self _createDirectoryAtPath:basePath]; return basePath; } + (NSURL *)applicationTemporaryProcessSpecificURL { return [NSURL fileURLWithPath:self.applicationTemporaryProcessSpecific isDirectory:YES]; } + (NSString *)bundledExtensions { return self.bundledExtensionsURL.path; } + (NSURL *)bundledExtensionsURL { NSURL *baseURL = self.applicationResourcesURL; return [baseURL URLByAppendingPathComponent:@"/Bundled Extensions/"]; } + (NSString *)bundledScripts { return self.bundledScriptsURL.path; } + (NSURL *)bundledScriptsURL { NSURL *baseURL = self.applicationResourcesURL; return [baseURL URLByAppendingPathComponent:@"/Bundled Scripts/"]; } + (NSString *)bundledThemes { return self.bundledThemesURL.path; } + (NSURL *)bundledThemesURL { NSURL *baseURL = self.applicationResourcesURL; return [baseURL URLByAppendingPathComponent:@"/Bundled Styles/"]; } + (nullable NSString *)customExtensions { NSURL *sourceURL = self.customExtensionsURL; if (sourceURL == nil) { return nil; } return sourceURL.path; } + (nullable NSURL *)customExtensionsURL { NSURL *sourceURL = self.groupContainerApplicationSupportURL; if (sourceURL == nil) { return nil; } NSURL *baseURL = [sourceURL URLByAppendingPathComponent:@"/Extensions/"]; [self _createDirectoryAtURL:baseURL]; return baseURL; } + (nullable NSString *)customScripts { NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSApplicationScriptsDirectory, NSUserDomainMask, YES); if (pathArray.count == 0) { return nil; } NSString *basePath = pathArray.firstObject; #if TEXTUAL_BUILT_INSIDE_SANDBOX == 0 [self _createDirectoryAtPath:basePath]; #endif return basePath; } + (nullable NSURL *)customScriptsURL { NSString *sourcePath = self.customScripts; if (sourcePath == nil) { return nil; } return [NSURL fileURLWithPath:sourcePath isDirectory:YES]; } + (nullable NSString *)customThemes { NSURL *sourceURL = self.customThemesURL; if (sourceURL == nil) { return nil; } return sourceURL.path; } + (nullable NSURL *)customThemesURL { NSURL *sourceURL = self.groupContainerApplicationSupportURL; if (sourceURL == nil) { return nil; } NSURL *baseURL = [sourceURL URLByAppendingPathComponent:@"/Styles/"]; [self _createDirectoryAtURL:baseURL]; return baseURL; } #pragma mark - #pragma mark System Specific + (nullable NSString *)systemApplications { NSArray *searchArray = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSSystemDomainMask, YES); if (searchArray.count == 0) { return nil; } return searchArray.firstObject; } + (nullable NSURL *)systemApplicationsURL { NSString *sourcePath = self.systemApplications; if (sourcePath == nil) { return nil; } return [NSURL fileURLWithPath:sourcePath isDirectory:YES]; } + (NSString *)systemDiagnosticReports { return @"/Library/Logs/DiagnosticReports"; } + (NSURL *)systemDiagnosticReportsURL { return [NSURL fileURLWithPath:self.systemDiagnosticReports isDirectory:YES]; } #pragma mark - #pragma mark User Specific + (nullable NSString *)userApplicationScripts { NSURL *sourceURL = self.userApplicationScriptsURL; if (sourceURL == nil) { return nil; } return sourceURL.path; } + (nullable NSURL *)userApplicationScriptsURL { NSURL *sourceURL = self.customScriptsURL; return [sourceURL URLByDeletingLastPathComponent]; } + (NSString *)userDiagnosticReports { return self.userDiagnosticReportsURL.path; } + (NSURL *)userDiagnosticReportsURL { NSURL *sourceURL = self.userHomeURL; return [sourceURL URLByAppendingPathComponent:@"/Library/Logs/DiagnosticReports"]; } + (nullable NSString *)userDownloads { NSArray *searchArray = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES); if (searchArray.count == 0) { return nil; } return searchArray.firstObject; } + (nullable NSURL *)userDownloadsURL { NSString *sourcePath = self.userDownloads; if (sourcePath == nil) { return nil; } return [NSURL fileURLWithPath:sourcePath isDirectory:YES]; } + (NSString *)userHome { return [NSFileManager pathOfHomeDirectoryOutsideSandbox]; } + (NSURL *)userHomeURL { return [NSFileManager URLOfHomeDirectoryOutsideSandbox]; } + (nullable NSString *)userPreferences { NSArray *searchArray = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); if (searchArray.count == 0) { return nil; } return [searchArray.firstObject stringByAppendingPathComponent:@"/Preferences/"]; } + (nullable NSURL *)userPreferencesURL { NSString *sourcePath = self.userPreferences; if (sourcePath == nil) { return nil; } return [NSURL fileURLWithPath:sourcePath isDirectory:YES]; } @end #pragma mark - #pragma mark Transcript URL @implementation TPCPathInfo (TPCPathInfoTranscriptFolderExtension) static NSURL * _Nullable _transcriptFolderURL = nil; + (nullable NSString *)transcriptFolder { return _transcriptFolderURL.path; } + (nullable NSURL *)transcriptFolderURL { return _transcriptFolderURL; } + (void)setTranscriptFolderURL:(nullable NSData *)transcriptFolderURL { if ( _transcriptFolderURL) { [_transcriptFolderURL stopAccessingSecurityScopedResource]; _transcriptFolderURL = nil; } [RZUserDefaults() setObject:transcriptFolderURL forKey:@"LogTranscriptDestinationSecurityBookmark_5"]; [self startUsingTranscriptFolderURL]; } + (void)warnUserAboutStaleTranscriptFolderURL { /* If logging isn't enabled, then we don't inform user of stale bookmarks because they probably aren't interested in that fact. */ if ([TPCPreferences logToDisk] == NO) { return; } [TDCAlert alertWithMessage:TXTLS(@"Prompts[atn-1c]") title:TXTLS(@"Prompts[b7o-v4]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } + (void)startUsingTranscriptFolderURL { /* Even if user has logging disabled, we still start using the bookmark so that we can present it in the Preferences window while the pop up for selecting a new location is disabled. */ NSData *bookmark = [RZUserDefaults() dataForKey:@"LogTranscriptDestinationSecurityBookmark_5"]; if (bookmark == nil) { return; } BOOL resolvedBookmarkIsStale = YES; NSError *resolvedBookmarkError = nil; NSURL *resolvedBookmark = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&resolvedBookmarkIsStale error:&resolvedBookmarkError]; if (resolvedBookmarkIsStale) { /* "On return, if YES, the bookmark data is stale. Your app should create a new bookmark using the returned URL and use it in place of any stored copies of the existing bookmark." */ NSData *newBookmark = [resolvedBookmark bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:NULL]; if (newBookmark) { [self setTranscriptFolderURL:newBookmark]; } else { [self warnUserAboutStaleTranscriptFolderURL]; } return; } if (resolvedBookmark == nil) { LogToConsoleError("Error creating bookmark for URL: %{public}@", resolvedBookmarkError.localizedDescription); [self warnUserAboutStaleTranscriptFolderURL]; return; } _transcriptFolderURL = resolvedBookmark; if ([_transcriptFolderURL startAccessingSecurityScopedResource] == NO) { LogToConsoleError("Failed to access bookmark"); } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/TPCPreferencesImportExport.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCClientConfig.h" #import "IRCClientPrivate.h" #import "IRCWorldPrivate.h" #import "TXMasterController.h" #import "TDCAlert.h" #import "TLOLocalization.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesReload.h" #import "TPCPreferencesUserDefaultsLocal.h" #import "TVCMainWindowPrivate.h" #import "TVCMainWindowLoadingScreen.h" #import "TVCServerList.h" #import "TPCPreferencesImportExportPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TPCPreferencesUserDefaults () - (void)_migrateObject:(nullable id)value forKey:(NSString *)defaultName; @end @implementation TPCPreferencesImportExport + (void)importInWindow:(NSWindow *)window { NSParameterAssert(window != nil); [TDCAlert alertSheetWithWindow:window body:TXTLS(@"Prompts[jsh-1a]") title:TXTLS(@"Prompts[itb-3x]") defaultButton:TXTLS(@"Prompts[502-6h]") alternateButton:TXTLS(@"Prompts[qso-2g]") otherButton:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { [self importPreflight:buttonClicked]; }]; } + (void)importPreflight:(TDCAlertResponse)buttonPressed { if (buttonPressed != TDCAlertResponseDefault) { return; } NSOpenPanel *d = [NSOpenPanel openPanel]; d.canChooseFiles = YES; d.canChooseDirectories = NO; d.canCreateDirectories = NO; d.resolvesAliases = YES; d.allowsMultipleSelection = NO; [d beginWithCompletionHandler:^(NSInteger returnCode) { if (returnCode == NSModalResponseOK) { NSURL *pathURL = d.URLs[0]; [self importPostflight:pathURL]; } }]; } + (BOOL)importPostflightBackupPreferences { NSString *sourcePath = NSHomeDirectory(); NSString *basePath = [NSString stringWithFormat:@"/Textual-importBackup-%@.plist", [NSString stringWithUUID]]; NSString *backupPath = [sourcePath stringByAppendingPathComponent:basePath]; return [self exportPostflightForPath:backupPath filterJunk:NO]; } + (void)importPostflight:(NSURL *)pathURL { XRPerformBlockAsynchronouslyOnMainQueue(^{ [self _importPostflight:pathURL]; }); } + (void)_importPostflight:(NSURL *)pathURL { /* Create a backup of the old configuration */ if ([self importPostflightBackupPreferences] == NO) { return; } /* Begin import */ NSData *fileContents = [NSData dataWithContentsOfURL:pathURL]; NSError *parseError = nil; NSDictionary *propertyList = [NSPropertyListSerialization propertyListWithData:fileContents options:NSPropertyListImmutable format:NULL error:&parseError]; /* Perform actual import if we have the dictionary. */ if (propertyList == nil) { if (parseError) { LogToConsoleError("Import failed: %{public}@", parseError.localizedDescription); } return; } /* The loading screen is a generic way to show something during import */ [mainWindowLoadingScreen() showProgressViewWithReason:TXTLS(@"TVCMainWindow[5g1-i9]")]; [worldController() setIsImportingConfiguration:YES]; [mainWindowServerList() beginUpdates]; /* Import data */ [self importContentsOfDictionary:propertyList reloadPreferences:NO]; /* Do not push the loading screen right away. Add a little delay to give everything a chance to settle down before presenting the changes to the user. */ [self performSelectorInCommonModes:@selector(importPostflightCleanup:) withObject:propertyList.allKeys afterDelay:2.0]; } + (void)importContentsOfDictionary:(NSDictionary *)aDict { [self importContentsOfDictionary:aDict reloadPreferences:YES]; } + (void)importContentsOfDictionary:(NSDictionary *)aDict reloadPreferences:(BOOL)reloadPreferences { NSParameterAssert(aDict != nil); [aDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id object, BOOL *stop) { [self import:object withKey:key]; }]; if (reloadPreferences) { [TPCPreferences performReloadActionForKeys:aDict.allKeys]; } } + (void)import:(id)object withKey:(NSString *)key { NSParameterAssert(key != nil); if ([key isEqualToString:TPCPreferencesThemeNameDefaultsKey]) { if ([object isKindOfClass:[NSString class]] == NO) { return; } [TPCPreferences setThemeNameWithExistenceCheck:object]; } else if ([key isEqualToString:TPCPreferencesThemeFontNameDefaultsKey]) { if ([object isKindOfClass:[NSString class]] == NO) { return; } [TPCPreferences setThemeChannelViewFontNameWithExistenceCheck:object]; } else if ([key isEqualToString:IRCWorldClientListDefaultsKey]) { if ([object isKindOfClass:[NSArray class]] == NO) { return; } [object enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { [self importClientConfiguration:object]; }]; } else { [RZUserDefaults() _migrateObject:object forKey:key]; } } + (void)importClientConfiguration:(NSDictionary *)config { NSParameterAssert(config != nil); IRCClientConfig *clientConfig = [[IRCClientConfig alloc] initWithDictionary:config]; IRCClient *client = [worldController() findClientWithId:clientConfig.uniqueIdentifier]; if (client) { [client updateConfig:clientConfig]; } else { [worldController() createClientWithConfig:clientConfig reload:YES]; } } + (void)importPostflightCleanup:(NSArray *)changedKeys { [TPCPreferences performReloadActionForKeys:changedKeys]; [mainWindowServerList() endUpdates]; [worldController() setIsImportingConfiguration:NO]; [mainWindow() reloadLoadingScreen]; } #pragma mark - #pragma mark Export + (BOOL)isKeyNameSupposedToBeIgnored:(NSString *)key { return [TPCPreferencesUserDefaults keyIsExcludedFromExportImport:key]; } + (NSDictionary *)exportedPreferencesDictionary { return [self exportedPreferencesDictionary:YES filterDefaults:YES]; } + (NSDictionary *)exportedPreferencesDictionary:(BOOL)filterJunk { return [self exportedPreferencesDictionary:filterJunk filterDefaults:filterJunk]; } + (NSDictionary *)exportedPreferencesDictionary:(BOOL)filterJunk filterDefaults:(BOOL)filterDefaults { /* Combine list of keys to strip */ NSMutableArray *keysToStrip = [NSMutableArray array]; NSDictionary *argumentsDomain = [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSArgumentDomain]; [keysToStrip addObjectsFromArray:argumentsDomain.allKeys]; if (filterDefaults) { NSDictionary *defaultsDomain = [TPCPreferences defaultPreferences]; [keysToStrip addObjectsFromArray:defaultsDomain.allKeys]; } if (filterJunk) { NSDictionary *globalsDomain = [[NSUserDefaults standardUserDefaults] persistentDomainForName:NSGlobalDomain]; [keysToStrip addObjectsFromArray:globalsDomain.allKeys]; } /* Create mutable copy of preferences and strip keys */ NSDictionary *exportedPreferences = [RZUserDefaults() dictionaryRepresentation]; NSMutableDictionary *finalDictionary = [exportedPreferences mutableCopy]; [finalDictionary removeObjectsForKeys:keysToStrip]; /* Strip keys that must be checked dynamically */ if (filterJunk) { NSSet *keysToStrip2 = [finalDictionary keysOfEntriesPassingTest:^BOOL(NSString *key, id object, BOOL *stop) { return [self isKeyNameSupposedToBeIgnored:key]; }]; [finalDictionary removeObjectsForKeys:keysToStrip2.allObjects]; } return [finalDictionary copy]; } + (void)exportInWindow:(NSWindow *)window { NSParameterAssert(window != nil); [TDCAlert alertSheetWithWindow:window body:TXTLS(@"Prompts[syp-al]") title:TXTLS(@"Prompts[1fm-up]") defaultButton:TXTLS(@"Prompts[vun-f0]") alternateButton:TXTLS(@"Prompts[qso-2g]") otherButton:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { [self exportPreflight:buttonClicked]; }]; } + (void)exportPreflight:(TDCAlertResponse)buttonPressed { if (buttonPressed != TDCAlertResponseDefault) { return; } NSSavePanel *d = [NSSavePanel savePanel]; d.canCreateDirectories = YES; d.nameFieldStringValue = TXLocalizationNotNeeded(@"TextualPreferences.plist"); [d beginWithCompletionHandler:^(NSInteger returnCode) { if (returnCode == NSModalResponseOK) { NSURL *pathURL = d.URL; [self exportPostflightForURL:pathURL filterJunk:YES]; } }]; } + (BOOL)exportPostflightForPath:(NSString *)path { return [self exportPostflightForPath:path filterJunk:YES]; } + (BOOL)exportPostflightForURL:(NSURL *)pathURL { return [self exportPostflightForURL:pathURL filterJunk:YES]; } + (BOOL)exportPostflightForPath:(NSString *)path filterJunk:(BOOL)filterJunk { NSParameterAssert(path != nil); NSURL *pathURL = [NSURL fileURLWithPath:path]; return [self exportPostflightForURL:pathURL filterJunk:filterJunk]; } + (BOOL)exportPostflightForURL:(NSURL *)pathURL filterJunk:(BOOL)filterJunk { NSParameterAssert(pathURL != nil); NSDictionary *exportedPreferences = [self exportedPreferencesDictionary:filterJunk]; /* The export will be saved as binary. Two reasons: 1) Discourages user from trying to tamper with stuff. 2) Smaller, faster. Mostly #1. */ NSError *parseError = nil; NSData *propertyList = [NSPropertyListSerialization dataWithPropertyList:exportedPreferences format:NSPropertyListBinaryFormat_v1_0 options:0 error:&parseError]; if (propertyList == nil) { if (parseError) { LogToConsoleError("Error Creating Property List: %{public}@", parseError.localizedDescription); } return NO; } BOOL writeResult = [propertyList writeToURL:pathURL atomically:YES]; if (writeResult == NO) { LogToConsoleError("Write failed"); return NO; } return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/TPCPreferencesLocal.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXGlobalModels.h" #import "IRCWorld.h" #import "IRCUserNicknameColorStyleGeneratorPrivate.h" #import "TPCApplicationInfoPrivate.h" #import "TPCPathInfoPrivate.h" #import "TPCPreferencesUserDefaultsLocal.h" #import "TPCResourceManager.h" #import "TPCThemeController.h" #import "TPCTheme.h" #import "TPCPreferencesLocalPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TPCPreferencesThemeNameDefaultsKey = @"Theme -> Name"; NSString * const TPCPreferencesThemeFontNameDefaultsKey = @"Theme -> Font Name"; NSString * const TPCPreferencesThemeFontSizeDefaultsKey = @"Theme -> Font Size"; NSString * const TPCPreferencesThemeNameMissingLocallyDefaultsKey = @"Theme -> Name -> Did Not Exist During Last Sync"; NSString * const TPCPreferencesThemeFontNameMissingLocallyDefaultsKey = @"Theme -> Font Name -> Did Not Exist During Last Sync"; NSUInteger const TPCPreferencesDictionaryVersion = 602; @implementation TPCPreferences (TPCPreferencesLocal) #pragma mark - #pragma mark Default Identity + (NSString *)_defaultNicknamePrefix { return [self defaultPreferences][@"DefaultIdentity -> Nickname"]; } + (void)_populateDefaultNickname { /* Using "Guest" as the default nickname may create conflicts as nickname guesses are exhausted while appending underscores. To fix this, a random number is appended to the end of the default nickname. */ NSString *nicknamePrefix = [self _defaultNicknamePrefix]; NSString *nickname = [nicknamePrefix stringByAppendingFormat:@"%lu", TXRandomNumber(100)]; [RZUserDefaults() registerDefaults:@{@"DefaultIdentity -> Nickname" : nickname}]; } + (NSString *)defaultNickname { return [RZUserDefaults() objectForKey:@"DefaultIdentity -> Nickname"]; } + (nullable NSString *)defaultAwayNickname { return [RZUserDefaults() objectForKey:@"DefaultIdentity -> AwayNickname"]; } + (NSString *)defaultUsername { return [RZUserDefaults() objectForKey:@"DefaultIdentity -> Username"]; } + (NSString *)defaultRealName { return [RZUserDefaults() objectForKey:@"DefaultIdentity -> Realname"]; } #pragma mark - #pragma mark General Preferences + (NSUInteger)autojoinMaximumChannelJoins { return [RZUserDefaults() unsignedIntegerForKey:@"AutojoinMaximumChannelJoinCount"]; } + (NSTimeInterval)autojoinDelayBetweenChannelJoins { return [RZUserDefaults() doubleForKey:@"AutojoinDelayBetweenChannelJoins"]; } + (NSTimeInterval)autojoinDelayAfterIdentification { return [RZUserDefaults() doubleForKey:@"AutojoinDelayAfterIdentification"]; } + (NSString *)defaultKickMessage { return [RZUserDefaults() objectForKey:@"ChannelOperatorDefaultLocalization -> Kick Reason"]; } + (NSString *)IRCopDefaultKillMessage { return [RZUserDefaults() objectForKey:@"IRCopDefaultLocalizaiton -> Kill Reason"]; } + (NSString *)IRCopDefaultGlineMessage { return [RZUserDefaults() objectForKey:@"IRCopDefaultLocalizaiton -> G:Line Reason"]; } + (NSString *)IRCopDefaultShunMessage { return [RZUserDefaults() objectForKey:@"IRCopDefaultLocalizaiton -> Shun Reason"]; } + (nullable NSString *)masqueradeCTCPVersion { return [RZUserDefaults() objectForKey:@"ApplicationCTCPVersionMasquerade"]; } + (BOOL)channelNavigationIsServerSpecific { /* TDCChannelSpotlightController is observing this key with it hard coded. If changed here, change there as well. */ return [RZUserDefaults() boolForKey:@"ChannelNavigationIsServerSpecific"]; } + (BOOL)setAwayOnScreenSleep { return [RZUserDefaults() boolForKey:@"SetAwayOnScreenSleep"]; } + (BOOL)disconnectOnSleep { return [RZUserDefaults() boolForKey:@"AutomaticallyDisconnectForSleepMode"]; } + (BOOL)disableSidebarTranslucency { return [RZUserDefaults() boolForKey:@"DisableSidebarTranslucency"]; } + (BOOL)hideMainWindowSegmentedController { return [RZUserDefaults() boolForKey:@"DisableMainWindowSegmentedController"]; } + (BOOL)logHighlights { return [RZUserDefaults() boolForKey:@"LogHighlights"]; } + (BOOL)clearAllConnections { return [RZUserDefaults() boolForKey:@"ApplyCommandToAllConnections -> clearall"]; } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 + (void)setTextEncryptionIsOpportunistic:(BOOL)textEncryptionIsOpportunistic { [RZUserDefaults() setBool:textEncryptionIsOpportunistic forKey:@"Off-the-Record Messaging -> Automatically Enable Service"]; } + (BOOL)textEncryptionIsOpportunistic { return [RZUserDefaults() boolForKey:@"Off-the-Record Messaging -> Automatically Enable Service"]; } + (void)setTextEncryptionIsRequired:(BOOL)textEncryptionIsRequired { [RZUserDefaults() setBool:textEncryptionIsRequired forKey:@"Off-the-Record Messaging -> Require Encryption"]; } + (BOOL)textEncryptionIsRequired { return [RZUserDefaults() boolForKey:@"Off-the-Record Messaging -> Require Encryption"]; } + (void)setTextEncryptionIsEnabled:(BOOL)textEncryptionIsEnabled { [RZUserDefaults() setBool:textEncryptionIsEnabled forKey:@"Off-the-Record Messaging -> Enable Encryption"]; } + (BOOL)textEncryptionIsEnabled { return [RZUserDefaults() boolForKey:@"Off-the-Record Messaging -> Enable Encryption"]; } #endif + (BOOL)enableEchoMessageCapability { // return [RZUserDefaults() boolForKey:@"IRC -> Enable echo-message Capability"]; return NO; } + (BOOL)displayServerMOTD { return [RZUserDefaults() boolForKey:@"DisplayServerMessageOfTheDayOnConnect"]; } + (BOOL)copyOnSelect { return [RZUserDefaults() boolForKey:@"CopyTextSelectionOnMouseUp"]; } + (BOOL)replyToCTCPRequests { return [RZUserDefaults() boolForKey:@"ReplyUnignoredExternalCTCPRequests"]; } + (BOOL)autoAddScrollbackMark { return [RZUserDefaults() boolForKey:@"AutomaticallyAddScrollbackMarker"]; } + (BOOL)removeAllFormatting { return [RZUserDefaults() boolForKey:@"RemoveIRCTextFormatting"]; } + (BOOL)automaticallyDetectHighlightSpam { return [RZUserDefaults() boolForKey:@"AutomaticallyDetectHighlightSpam"]; } + (BOOL)disableNicknameColorHashing { return [RZUserDefaults() boolForKey:@"DisableRemoteNicknameColorHashing"]; } + (BOOL)conversationTrackingIncludesUserModeSymbol { return [RZUserDefaults() boolForKey:@"ConversationTrackingIncludesUserModeSymbol"]; } + (BOOL)rightToLeftFormatting { return [RZUserDefaults() boolForKey:@"RightToLeftTextFormatting"]; } + (BOOL)displayDockBadge { return [RZUserDefaults() boolForKey:@"DisplayDockBadges"]; } + (BOOL)amsgAllConnections { return [RZUserDefaults() boolForKey:@"ApplyCommandToAllConnections -> amsg"]; } + (BOOL)awayAllConnections { return [RZUserDefaults() boolForKey:@"ApplyCommandToAllConnections -> away"]; } + (BOOL)giveFocusOnMessageCommand { return [RZUserDefaults() boolForKey:@"FocusSelectionOnMessageCommandExecution"]; } + (BOOL)memberListSortFavorsServerStaff { return [RZUserDefaults() boolForKey:@"MemberListSortFavorsServerStaff"]; } + (BOOL)memberListUpdatesUserInfoPopoverOnScroll { return [RZUserDefaults() boolForKey:@"MemberListUpdatesUserInfoPopoverOnScroll"]; } + (BOOL)memberListDisplayNoModeSymbol { return [RZUserDefaults() boolForKey:@"DisplayUserListNoModeSymbol"]; } + (BOOL)postNotificationsWhileInFocus { return [RZUserDefaults() boolForKey:@"PostNotificationsWhileInFocus"]; } + (BOOL)automaticallyFilterUnicodeTextSpam { return [RZUserDefaults() boolForKey:@"AutomaticallyFilterUnicodeTextSpam"]; } + (BOOL)nickAllConnections { return [RZUserDefaults() boolForKey:@"ApplyCommandToAllConnections -> nick"]; } + (BOOL)confirmQuit { return [RZUserDefaults() boolForKey:@"ConfirmApplicationQuit"]; } + (BOOL)rememberServerListQueryStates { return [RZUserDefaults() boolForKey:@"ServerListRetainsQueriesBetweenRestarts"]; } + (BOOL)rejoinOnKick { return [RZUserDefaults() boolForKey:@"RejoinChannelOnLocalKick"]; } + (BOOL)reloadScrollbackOnLaunch { return [RZUserDefaults() boolForKey:@"ReloadScrollbackOnLaunch"]; } + (BOOL)autoJoinOnInvite { return [RZUserDefaults() boolForKey:@"AutojoinChannelOnInvite"]; } + (BOOL)connectOnDoubleclick { return [RZUserDefaults() boolForKey:@"ServerListDoubleClickConnectServer"]; } + (BOOL)disconnectOnDoubleclick { return [RZUserDefaults() boolForKey:@"ServerListDoubleClickDisconnectServer"]; } + (BOOL)joinOnDoubleclick { return [RZUserDefaults() boolForKey:@"ServerListDoubleClickJoinChannel"]; } + (BOOL)leaveOnDoubleclick { return [RZUserDefaults() boolForKey:@"ServerListDoubleClickLeaveChannel"]; } + (BOOL)logToDisk { return [RZUserDefaults() boolForKey:@"LogTranscript"]; } + (void)setLogToDisk:(BOOL)logToDisk { [RZUserDefaults() setBool:logToDisk forKey:@"LogTranscript"]; } + (BOOL)logToDiskIsEnabled { return ([RZUserDefaults() boolForKey:@"LogTranscript"] && [TPCPathInfo transcriptFolderURL] != nil); } + (BOOL)openBrowserInBackground { return [RZUserDefaults() boolForKey:@"OpenClickedLinksInBackgroundBrowser"]; } + (BOOL)showDateChanges { return [RZUserDefaults() boolForKey:@"DisplayEventInLogView -> Date Changes"]; } + (void)setShowInlineMedia:(BOOL)showInlineMedia { [RZUserDefaults() setBool:showInlineMedia forKey:@"DisplayEventInLogView -> Inline Media"]; } + (BOOL)showInlineMedia { return [RZUserDefaults() boolForKey:@"DisplayEventInLogView -> Inline Media"]; } + (BOOL)showJoinLeave { return [RZUserDefaults() boolForKey:@"DisplayEventInLogView -> Join, Part, Quit"]; } + (BOOL)commandReturnSendsMessageAsAction { return [RZUserDefaults() boolForKey:@"CommandReturnSendsMessageAsAction"]; } + (BOOL)controlEnterSendsMessage { return [RZUserDefaults() boolForKey:@"ControlEnterSendsMessage"]; } + (BOOL)displayPublicMessageCountOnDockBadge { return [RZUserDefaults() boolForKey:@"DisplayPublicMessageCountInDockBadge"]; } + (void)setHighlightCurrentNickname:(BOOL)highlightCurrentNickname { [RZUserDefaults() setBool:highlightCurrentNickname forKey:@"TrackNicknameHighlightsOfLocalUser"]; } + (BOOL)highlightCurrentNickname { return [RZUserDefaults() boolForKey:@"TrackNicknameHighlightsOfLocalUser"]; } + (BOOL)inputHistoryIsChannelSpecific { return [RZUserDefaults() boolForKey:@"SaveInputHistoryPerSelection"]; } + (CGFloat)swipeMinimumLength { return [RZUserDefaults() doubleForKey:@"SwipeMinimumLength"]; } + (NSUInteger)trackUserAwayStatusMaximumChannelSize { return [RZUserDefaults() unsignedIntegerForKey:@"TrackUserAwayStatusMaximumChannelSize"]; } + (TXTabKeyAction)tabKeyAction { return (TXTabKeyAction)[RZUserDefaults() unsignedIntegerForKey:@"Keyboard -> Tab Key Action"]; } + (TXNicknameHighlightMatchType)highlightMatchingMethod { return (TXNicknameHighlightMatchType)[RZUserDefaults() unsignedIntegerForKey:@"NicknameHighlightMatchingType"]; } + (TXUserDoubleClickAction)userDoubleClickOption { return (TXUserDoubleClickAction)[RZUserDefaults() unsignedIntegerForKey:@"UserListDoubleClickAction"]; } + (TXNoticeSendLocation)locationToSendNotices { return (TXNoticeSendLocation)[RZUserDefaults() unsignedIntegerForKey:@"DestinationOfNonserverNotices"]; } + (void)setLocationToSendNotices:(TXNoticeSendLocation)locationToSendNotices { [RZUserDefaults() setUnsignedInteger:locationToSendNotices forKey:@"DestinationOfNonserverNotices"]; } + (TXCommandWKeyAction)commandWKeyAction { return (TXCommandWKeyAction)[RZUserDefaults() unsignedIntegerForKey:@"Keyboard -> Command+W Key Action"]; } + (TXHostmaskBanFormat)banFormat { return (TXHostmaskBanFormat)[RZUserDefaults() unsignedIntegerForKey:@"DefaultBanCommandHostmaskFormat"]; } + (TVCMainWindowTextViewFontSize)mainTextViewFontSize { return (TVCMainWindowTextViewFontSize)[RZUserDefaults() unsignedIntegerForKey:@"Main Input Text Field -> Font Size"]; } + (BOOL)focusMainTextViewOnSelectionChange { return [RZUserDefaults() boolForKey:@"Main Input Text Field -> Focus When Changing Views"]; } + (BOOL)preferModernCiphers { /* The name of this preference is somewhat of a misnomer. Modern ciphers are always used, regardless of its value. This preference defines whether the "deprecated" ciphers are used as well. Whether we should prefer modern only. */ return [RZUserDefaults() boolForKey:@"PreferModernCiphers"]; } + (BOOL)preferModernSockets { return [RZUserDefaults() boolForKey:@"PreferModernSockets"]; } #pragma mark - #pragma mark App Nap + (BOOL)appNapEnabled { return ([[NSUserDefaults standardUserDefaults] boolForKey:@"NSAppSleepDisabled"] == NO); } + (void)setAppNapEnabled:(BOOL)appNapEnabled { [[NSUserDefaults standardUserDefaults] setBool:(appNapEnabled == NO) forKey:@"NSAppSleepDisabled"]; } #pragma mark - #pragma mark Updates #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 + (BOOL)receiveBetaUpdates { return [RZUserDefaults() boolForKey:@"ReceiveBetaUpdates"]; } #endif #pragma mark - #pragma mark Developer Mode + (void)setDeveloperModeEnabled:(BOOL)developerModeEnabled { [RZUserDefaults() setBool:developerModeEnabled forKey:@"TextualDeveloperEnvironment"]; } + (BOOL)developerModeEnabled { return [RZUserDefaults() boolForKey:@"TextualDeveloperEnvironment"]; } #pragma mark - #pragma mark Theme + (void)setAppearance:(TXPreferredAppearance)appearance { [RZUserDefaults() setUnsignedInteger:appearance forKey:@"Appearance"]; } + (TXPreferredAppearance)appearance { return (TXPreferredAppearance)[RZUserDefaults() unsignedIntegerForKey:@"Appearance"]; } + (BOOL)invertSidebarColors { return [RZUserDefaults() boolForKey:@"InvertSidebarColors"]; } + (NSString *)themeNameDefault { return [self defaultPreferences][TPCPreferencesThemeNameDefaultsKey]; } + (NSString *)themeName { return [RZUserDefaults() objectForKey:TPCPreferencesThemeNameDefaultsKey]; } + (void)setThemeName:(NSString *)value { NSParameterAssert(value != nil); [RZUserDefaults() setObject:value forKey:TPCPreferencesThemeNameDefaultsKey]; [RZUserDefaults() removeObjectForKey:TPCPreferencesThemeNameMissingLocallyDefaultsKey]; } + (void)setThemeNameWithExistenceCheck:(NSString *)value { NSParameterAssert(value != nil); if ([themeController() themeExists:value]) { [self setThemeName:value]; } else { [RZUserDefaults() setBool:YES forKey:TPCPreferencesThemeNameMissingLocallyDefaultsKey]; } } + (NSString *)themeChannelViewFontNameDefault { return [self defaultPreferences][TPCPreferencesThemeFontNameDefaultsKey]; } + (NSString *)themeChannelViewFontName { return [RZUserDefaults() objectForKey:TPCPreferencesThemeFontNameDefaultsKey]; } + (void)setThemeChannelViewFontName:(NSString *)value { NSParameterAssert(value != nil); [RZUserDefaults() setObject:value forKey:TPCPreferencesThemeFontNameDefaultsKey]; [RZUserDefaults() removeObjectForKey:TPCPreferencesThemeFontNameMissingLocallyDefaultsKey]; } + (void)setThemeChannelViewFontNameWithExistenceCheck:(NSString *)value { NSParameterAssert(value != nil); if ([NSFont fontIsAvailable:value]) { [self setThemeChannelViewFontName:value]; } else { [RZUserDefaults() setBool:YES forKey:TPCPreferencesThemeFontNameMissingLocallyDefaultsKey]; } } + (CGFloat)themeChannelViewFontSize { return [RZUserDefaults() doubleForKey:@"Theme -> Font Size"]; } + (void)setThemeChannelViewFontSize:(CGFloat)value { [RZUserDefaults() setDouble:value forKey:@"Theme -> Font Size"]; } + (nullable NSFont *)themeChannelViewFont { return [NSFont fontWithName:[self themeChannelViewFontName] size:[self themeChannelViewFontSize]]; } + (BOOL)themeChannelViewFontPreferenceUserConfigurable { return [RZUserDefaults() boolForKey:@"Theme -> Channel Font Preference Enabled"]; } + (void)setThemeChannelViewFontPreferenceUserConfigurable:(BOOL)themeChannelViewFontPreferenceUserConfigurable { [RZUserDefaults() registerDefault:@(themeChannelViewFontPreferenceUserConfigurable) forKey:@"Theme -> Channel Font Preference Enabled"]; } + (NSString *)themeNicknameFormatDefault { return [self defaultPreferences][@"Theme -> Nickname Format"]; } + (NSString *)themeNicknameFormat { return [RZUserDefaults() objectForKey:@"Theme -> Nickname Format"]; } + (BOOL)themeNicknameFormatPreferenceUserConfigurable { return [RZUserDefaults() boolForKey:@"Theme -> Nickname Format Preference Enabled"]; } + (void)setThemeNicknameFormatPreferenceUserConfigurable:(BOOL)themeNicknameFormatPreferenceUserConfigurable { [RZUserDefaults() registerDefault:@(themeNicknameFormatPreferenceUserConfigurable) forKey:@"Theme -> Nickname Format Preference Enabled"]; } + (NSString *)themeTimestampFormatDefault { return [self defaultPreferences][@"Theme -> Timestamp Format"]; } + (NSString *)themeTimestampFormat { return [RZUserDefaults() objectForKey:@"Theme -> Timestamp Format"]; } + (BOOL)themeTimestampFormatPreferenceUserConfigurable { return [RZUserDefaults() boolForKey:@"Theme -> Timestamp Format Preference Enabled"]; } + (void)setThemeTimestampFormatPreferenceUserConfigurable:(BOOL)themeTimestampFormatPreferenceUserConfigurable { [RZUserDefaults() registerDefault:@(themeTimestampFormatPreferenceUserConfigurable) forKey:@"Theme -> Timestamp Format Preference Enabled"]; } + (nullable NSString *)themeUserStyleSheetRules { return [RZUserDefaults() objectForKey:@"Theme -> User Style Sheet Rules"]; } + (void)setThemeUserStyleSheetRules:(nullable NSString *)themeUserStyleSheetRules { [RZUserDefaults() setObject:themeUserStyleSheetRules forKey:@"Theme -> User Style Sheet Rules"]; } + (CGFloat)mainWindowTransparency { return [RZUserDefaults() doubleForKey:@"MainWindowTransparencyLevel"]; } + (BOOL)automaticallyReloadCustomThemesWhenTheyChange { return [RZUserDefaults() boolForKey:@"AutomaticallyReloadCustomThemesWhenTheyChange"]; } + (void)setWebKit2Enabled:(BOOL)webKit2Enabled { [RZUserDefaults() setBool:webKit2Enabled forKey:@"UsesWebKit2WhenAvailable"]; } + (BOOL)webKit2Enabled { return [RZUserDefaults() boolForKey:@"UsesWebKit2WhenAvailable"]; } + (BOOL)webKit2ProcessPoolSizeLimited { return [RZUserDefaults() boolForKey:@"WebViewProcessPoolSizeIsLimited"]; } + (BOOL)webKit2PreviewLinks { return [RZUserDefaults() boolForKey:@"WebViewPreviewLinks"]; } + (BOOL)themeChannelViewUsesCustomScrollers { return ([RZUserDefaults() boolForKey:@"WebViewDoNotUsesCustomScrollers"] == NO); } + (TXChannelViewArrangement)channelViewArrangement { return [RZUserDefaults() unsignedIntegerForKey:@"ChannelViewArrangement"]; } #pragma mark - #pragma mark Completion Suffix + (nullable NSString *)tabCompletionSuffix { return [RZUserDefaults() objectForKey:@"Keyboard -> Tab Key Completion Suffix"]; } + (void)setTabCompletionSuffix:(NSString *)value { NSParameterAssert(value != nil); [RZUserDefaults() setObject:value forKey:@"Keyboard -> Tab Key Completion Suffix"]; } + (BOOL)tabCompletionDoNotAppendWhitespace { return [RZUserDefaults() boolForKey:@"Tab Completion -> Do Not Use Whitespace for Missing Completion Suffix"]; } + (BOOL)tabCompletionCutForwardToFirstWhitespace { return [RZUserDefaults() boolForKey:@"Tab Completion -> Completion Suffix Cut Forward Until Space"]; } #pragma mark - #pragma mark File Transfers + (TXFileTransferRequestReply)fileTransferRequestReplyAction { return [RZUserDefaults() unsignedIntegerForKey:@"File Transfers -> File Transfer Request Reply Action"]; } + (TXFileTransferIPAddressMethodDetection)fileTransferIPAddressDetectionMethod { return [RZUserDefaults() unsignedIntegerForKey:@"File Transfers -> File Transfer IP Address Detection Method"]; } + (BOOL)fileTransferRequestsAreReversed { return [RZUserDefaults() boolForKey:@"File Transfers -> File Transfer Requests Use Reverse DCC"]; } + (BOOL)fileTransfersPreventIdleSystemSleep { return [RZUserDefaults() boolForKey:@"File Transfers -> Idle System Sleep Prevented During File Transfer"]; } + (uint16_t)fileTransferPortRangeStart { return [RZUserDefaults() unsignedShortForKey:@"File Transfers -> File Transfer Port Range Start"]; } + (void)setFileTransferPortRangeStart:(uint16_t)value { [RZUserDefaults() setUnsignedShort:value forKey:@"File Transfers -> File Transfer Port Range Start"]; } + (uint16_t)fileTransferPortRangeEnd { return [RZUserDefaults() unsignedShortForKey:@"File Transfers -> File Transfer Port Range End"]; } + (void)setFileTransferPortRangeEnd:(uint16_t)value { [RZUserDefaults() setUnsignedShort:value forKey:@"File Transfers -> File Transfer Port Range End"]; } + (nullable NSString *)fileTransferManuallyEnteredIPAddress { return [RZUserDefaults() objectForKey:@"File Transfers -> File Transfer Manually Entered IP Address"]; } + (nullable NSString *)fileTransferIPAddressInterfaceName { return [RZUserDefaults() objectForKey:@"File Transfers -> File Transfer IP Address Interface Name"]; } #pragma mark - #pragma mark Max Log Lines + (NSUInteger)scrollbackSaveLimit { return [RZUserDefaults() unsignedIntegerForKey:@"ScrollbackMaximumSavedLineCount"]; } + (void)setScrollbackSaveLimit:(NSUInteger)scrollbackSaveLimit { [RZUserDefaults() setUnsignedInteger:scrollbackSaveLimit forKey:@"ScrollbackMaximumSavedLineCount"]; } + (NSUInteger)scrollbackVisibleLimit { return [RZUserDefaults() unsignedIntegerForKey:@"ScrollbackMaximumVisibleLineCount"]; } + (void)setScrollbackVisibleLimit:(NSUInteger)scrollbackVisibleLimit { [RZUserDefaults() setUnsignedInteger:scrollbackVisibleLimit forKey:@"ScrollbackMaximumVisibleLineCount"]; } #pragma mark - #pragma mark Notifications + (BOOL)soundIsMuted { return [RZUserDefaults() boolForKey:@"Notification Sound Is Muted"]; } + (void)setSoundIsMuted:(BOOL)soundIsMuted { [RZUserDefaults() setBool:soundIsMuted forKey:@"Notification Sound Is Muted"]; } + (nullable NSString *)keyForEvent:(TXNotificationType)event category:(NSString *)category { NSParameterAssert(category != nil); NSString *returnValue = nil; switch (event) { #define _dv(key, value) case (key): { returnValue = (value); break; } _dv(TXNotificationTypeAddressBookMatch, @"NotificationType -> Address Book Match -> ") _dv(TXNotificationTypeChannelMessage, @"NotificationType -> Public Message -> ") _dv(TXNotificationTypeChannelNotice, @"NotificationType -> Public Notice -> ") _dv(TXNotificationTypeConnect, @"NotificationType -> Connected -> ") _dv(TXNotificationTypeDisconnect, @"NotificationType -> Disconnected -> ") _dv(TXNotificationTypeHighlight, @"NotificationType -> Highlight -> ") _dv(TXNotificationTypeInvite, @"NotificationType -> Channel Invitation -> ") _dv(TXNotificationTypeKick, @"NotificationType -> Kicked from Channel -> ") _dv(TXNotificationTypeNewPrivateMessage, @"NotificationType -> Private Message (New) -> ") _dv(TXNotificationTypePrivateMessage, @"NotificationType -> Private Message -> ") _dv(TXNotificationTypePrivateNotice, @"NotificationType -> Private Notice -> ") _dv(TXNotificationTypeFileTransferSendSuccessful, @"NotificationType -> Successful File Transfer (Sending) -> ") _dv(TXNotificationTypeFileTransferReceiveSuccessful, @"NotificationType -> Successful File Transfer (Receiving) -> ") _dv(TXNotificationTypeFileTransferSendFailed, @"NotificationType -> Failed File Transfer (Sending) -> ") _dv(TXNotificationTypeFileTransferReceiveFailed, @"NotificationType -> Failed File Transfer (Receiving) -> ") _dv(TXNotificationTypeFileTransferReceiveRequested, @"NotificationType -> File Transfer Request -> ") _dv(TXNotificationTypeUserJoined, @"NotificationType -> User Joined -> ") _dv(TXNotificationTypeUserParted, @"NotificationType -> User Parted -> ") _dv(TXNotificationTypeUserDisconnected, @"NotificationType -> User Disconnected -> ") #undef _dv } if (returnValue == nil) { return nil; } return [returnValue stringByAppendingString:category]; } + (nullable NSString *)soundForEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Sound"]; if (eventKey == nil) { return nil; } return [RZUserDefaults() objectForKey:eventKey]; } + (void)setSound:(nullable NSString *)value forEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Sound"]; if (eventKey == nil) { return; } [RZUserDefaults() setObject:value forKey:eventKey]; } + (BOOL)notificationEnabledForEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Enabled"]; if (eventKey == nil) { return NO; } return [RZUserDefaults() boolForKey:eventKey]; } + (void)setNotificationEnabled:(BOOL)value forEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Enabled"]; if (eventKey == nil) { return; } [RZUserDefaults() setBool:value forKey:eventKey]; } + (BOOL)disabledWhileAwayForEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Disable While Away"]; if (eventKey == nil) { return NO; } return [RZUserDefaults() boolForKey:eventKey]; } + (void)setDisabledWhileAway:(BOOL)value forEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Disable While Away"]; if (eventKey == nil) { return; } [RZUserDefaults() setBool:value forKey:eventKey]; } + (BOOL)bounceDockIconForEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Bounce Dock Icon"]; if (eventKey == nil) { return NO; } return [RZUserDefaults() boolForKey:eventKey]; } + (void)setBounceDockIcon:(BOOL)value forEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Bounce Dock Icon"]; if (eventKey == nil) { return; } [RZUserDefaults() setBool:value forKey:eventKey]; } + (BOOL)bounceDockIconRepeatedlyForEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Bounce Dock Icon Repeatedly"]; if (eventKey == nil) { return NO; } return [RZUserDefaults() boolForKey:eventKey]; } + (void)setBounceDockIconRepeatedly:(BOOL)value forEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Bounce Dock Icon Repeatedly"]; if (eventKey == nil) { return; } [RZUserDefaults() setBool:value forKey:eventKey]; } + (BOOL)speakEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Speak"]; if (eventKey == nil) { return NO; } return [RZUserDefaults() boolForKey:eventKey]; } + (void)setEventIsSpoken:(BOOL)value forEvent:(TXNotificationType)event { NSString *eventKey = [self keyForEvent:event category:@"Speak"]; if (eventKey == nil) { return; } [RZUserDefaults() setBool:value forKey:eventKey]; } + (BOOL)onlySpeakEventsForSelection { return [RZUserDefaults() boolForKey:@"OnlySpeakNotificationsForSelection"]; } + (void)setOnlySpeakEventsForSelection:(BOOL)onlySpeakEventsForSelection { [RZUserDefaults() setBool:onlySpeakEventsForSelection forKey:@"OnlySpeakNotificationsForSelection"]; } + (BOOL)channelMessageSpeakChannelName { NSString *eventKey = [self keyForEvent:TXNotificationTypeChannelMessage category:@"Speak Channel Name"]; return [RZUserDefaults() boolForKey:eventKey]; } + (void)setChannelMessageSpeakChannelName:(BOOL)channelMessageSpeakChannelName { NSString *eventKey = [self keyForEvent:TXNotificationTypeChannelMessage category:@"Speak Channel Name"]; [RZUserDefaults() setBool:channelMessageSpeakChannelName forKey:eventKey]; } + (BOOL)channelMessageSpeakNickname { NSString *eventKey = [self keyForEvent:TXNotificationTypeChannelMessage category:@"Speak Nickname"]; return [RZUserDefaults() boolForKey:eventKey]; } + (void)setChannelMessageSpeakNickname:(BOOL)channelMessageSpeakNickname { NSString *eventKey = [self keyForEvent:TXNotificationTypeChannelMessage category:@"Speak Nickname"]; [RZUserDefaults() setBool:channelMessageSpeakNickname forKey:eventKey]; } #pragma mark - #pragma mark World + (nullable NSArray *)clientList { return [RZUserDefaults() objectForKey:IRCWorldClientListDefaultsKey]; } + (void)setClientList:(nullable NSArray *)clientList { [RZUserDefaults() setObject:clientList forKey:IRCWorldClientListDefaultsKey]; } #pragma mark - #pragma mark Keywords static NSArray *_excludeKeywords = nil; static NSArray *_matchKeywords = nil; + (void)_loadExcludeKeywords { NSArray *keywordArrayIn = [RZUserDefaults() arrayForKey:@"Highlight List -> Excluded Matches"]; NSMutableArray *keywordArrayOut = [NSMutableArray array]; for (NSDictionary *keyword in keywordArrayIn) { NSString *s = keyword[@"string"]; if (s && s.length > 0) { [keywordArrayOut addObject:s]; } } _excludeKeywords = [keywordArrayOut copy]; } + (void)_loadMatchKeywords { NSArray *keywordArrayIn = [RZUserDefaults() arrayForKey:@"Highlight List -> Primary Matches"]; NSMutableArray *keywordArrayOut = [NSMutableArray array]; for (NSDictionary *keyword in keywordArrayIn) { NSString *s = keyword[@"string"]; if (s && s.length > 0) { [keywordArrayOut addObject:s]; } } _matchKeywords = [keywordArrayOut copy]; } + (void)_cleanUpKeywords:(NSString *)key { NSArray *keywordArrayIn = [RZUserDefaults() arrayForKey:key]; NSMutableArray *keywordArrayOut = [NSMutableArray array]; for (NSDictionary *keyword in keywordArrayIn) { NSString *s = keyword[@"string"]; if (s && s.length > 0) { [keywordArrayOut addObject:s]; } } [keywordArrayOut sortUsingSelector:@selector(caseInsensitiveCompare:)]; NSArray *arrayToSave = keywordArrayOut.stringArrayControllerObjects; [RZUserDefaults() setObject:arrayToSave forKey:key]; } + (void)cleanUpHighlightKeywords { [self _cleanUpKeywords:@"Highlight List -> Primary Matches"]; [self _cleanUpKeywords:@"Highlight List -> Excluded Matches"]; } + (nullable NSArray *)highlightMatchKeywords { return _matchKeywords; } + (nullable NSArray *)highlightExcludeKeywords { return _excludeKeywords; } #pragma mark - #pragma mark Key-Value Observing + (void)observeValueForKeyPath:(NSString *)key ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([key isEqualToString:@"Highlight List -> Primary Matches"]) { [self _loadMatchKeywords]; } else if ([key isEqualToString:@"Highlight List -> Excluded Matches"]) { [self _loadExcludeKeywords]; } } #pragma mark - #pragma mark Migration + (void)_migrateWorldControllerToVersion600 { #define _defaultsKey @"TPCPreferences -> Migration -> World Controller Migrated (600)" if ([RZUserDefaults() boolForKey:@"World Controller Migrated (600)"]) { [RZUserDefaults() removeObjectForKey:@"World Controller Migrated (600)"]; [RZUserDefaults() setBool:YES forKey:_defaultsKey]; } if ([RZUserDefaults() boolForKey:_defaultsKey]) { return; } BOOL clientListMigrated = [RZUserDefaults() boolForKey:_defaultsKey]; if (clientListMigrated) { return; } NSDictionary *worldController = [RZUserDefaults() dictionaryForKey:@"World Controller"]; NSArray *clientList = [worldController arrayForKey:@"clients"]; if (clientList.count > 0) { [self setClientList:clientList]; } BOOL soundIsMuted = [worldController boolForKey:@"soundIsMuted"]; if (soundIsMuted) { [self setSoundIsMuted:soundIsMuted]; } [RZUserDefaults() setBool:YES forKey:_defaultsKey]; #undef _defaultsKey } #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 + (void)_migrateSparkleConfigurationToVersion601 { #define _defaultsKey @"TPCPreferences -> Migration -> Sparkle (601)" BOOL sparkleMigrated = [RZUserDefaults() boolForKey:_defaultsKey]; if (sparkleMigrated) { return; } [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"SUEnableAutomaticChecks"]; [RZUserDefaults() setBool:YES forKey:_defaultsKey]; #undef _defaultsKey } #endif + (void)_migrateNicknameColorOverridesToVersion722 { /* Migrate from database that used NSArchiver to one that uses NSKeyedArchiver. */ #define _defaultsKey @"TPCPreferences -> Migration -> Nickname Color Style Overrides" BOOL overridesMigrated = [RZUserDefaults() boolForKey:_defaultsKey]; if (overridesMigrated) { return; } [IRCUserNicknameColorStyleGenerator migrateNicknameColorStyleOverrides]; [RZUserDefaults() setBool:YES forKey:_defaultsKey]; #undef _defaultsKey } + (void)_migrateAppearanceToVersion7011 /* 7.0.11 turned into 7.1.0 */ { #define _defaultsKey @"TPCPreferences -> Migration -> Appearance (7011)" BOOL appearanceMigrated = [RZUserDefaults() boolForKey:_defaultsKey]; if (appearanceMigrated) { return; } BOOL invertSidebarColors = [self invertSidebarColors]; if (invertSidebarColors) { [self setAppearance:TXPreferredAppearanceDark]; } [RZUserDefaults() setBool:YES forKey:_defaultsKey]; #undef _defaultsKey } #pragma mark - #pragma mark Dynamic Defaults + (void)registerWebKit2DynamicDefaults { /* The WebKit2 Web Inspector cannot work attached to Textual's main window. Whose fault this is isn't clear, but I do not have time to take a deep look at it at this time. To fix it temporarily, we always force it as window. To prevent the user breaking Textual by attaching it, we force reset the default here, every run. */ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"__WebInspectorPageGroupLevel1__.WebKit2InspectorStartsAttached"]; } + (void)registerPreferencesDictionaryVersion { /* We do not allow Textual to register a version lower than what is already set so that if the user opens an older version, we do not perform migrations more than once. Probably would have been smart to do this from the beginning. */ NSNumber *dictionaryVersion = [RZUserDefaults() objectForKey:@"TPCPreferencesDictionaryVersion"]; if (dictionaryVersion.integerValue >= TPCPreferencesDictionaryVersion) { return; } [RZUserDefaults() setUnsignedInteger:TPCPreferencesDictionaryVersion forKey:@"TPCPreferencesDictionaryVersion"]; } #pragma mark - #pragma mark Initialization + (NSDictionary *)defaultPreferences { /* Note added October 2017: I wrote this code a year ago and today (when the note was written), I took another look at it. 95% of defaults are registered with RZUserDefaults, not NSUserDefaults. So why has this code never created a problem? After performing more research, it turns out the registration domain is app wide. It doesn't care which instance of NSUserDefaults you set it on or read it from. It will be consistent app wide. This is nothing exciting. I documenting this more for my own sanity. */ return [RZUserDefaults() volatileDomainForName:NSRegistrationDomain]; } + (void)registerDynamicDefaults { [self _populateDefaultNickname]; [self registerWebKit2DynamicDefaults]; NSMutableDictionary *dynamicDefaults = [NSMutableDictionary dictionary]; [dynamicDefaults setBool:[TPCApplicationInfo sandboxEnabled] forKey:@"Security -> Sandbox Enabled"]; [dynamicDefaults setBool:NO forKey:@"System -> Running Mac OS Mountain Lion Or Newer"]; [dynamicDefaults setBool:TEXTUAL_RUNNING_ON_MAVERICKS forKey:@"System -> Running Mac OS Mavericks Or Newer"]; [dynamicDefaults setBool:TEXTUAL_RUNNING_ON_YOSEMITE forKey:@"System -> Running Mac OS Yosemite Or Newer"]; [dynamicDefaults setBool:TEXTUAL_RUNNING_ON_ELCAPITAN forKey:@"System -> Running Mac OS El Capitan Or Newer"]; [dynamicDefaults setBool:TEXTUAL_RUNNING_ON_SIERRA forKey:@"System -> Running Mac OS Sierra Or Newer"]; [dynamicDefaults setBool:TEXTUAL_RUNNING_ON_HIGHSIERRA forKey:@"System -> Running Mac OS High Sierra Or Newer"]; [dynamicDefaults setBool:TEXTUAL_RUNNING_ON_MOJAVE forKey:@"System -> Running Mac OS Mojave Or Newer"]; #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 [dynamicDefaults setBool:YES forKey:@"System -> 3rd-party Services -> Built with Sparkle Framework"]; #else [dynamicDefaults setBool:NO forKey:@"System -> 3rd-party Services -> Built with Sparkle Framework"]; #endif #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 [dynamicDefaults setBool:YES forKey:@"System -> Built with Off-the-Record Messaging Support"]; #else [dynamicDefaults setBool:NO forKey:@"System -> Built with Off-the-Record Messaging Support"]; #endif [RZUserDefaults() registerDefaults:dynamicDefaults]; [self registerPreferencesDictionaryVersion]; } + (void)registerDefaults { NSDictionary *localDefaults = [TPCResourceManager dictionaryFromResources:@"RegisteredUserDefaults" inDirectory:@"Preferences" cacheValue:NO]; [[NSUserDefaults standardUserDefaults] registerDefaults:localDefaults]; NSDictionary *containerDefaults = [TPCResourceManager dictionaryFromResources:@"RegisteredUserDefaultsInContainer" inDirectory:@"Preferences" cacheValue:NO]; [RZUserDefaults() registerDefaults:containerDefaults]; [self registerDynamicDefaults]; } + (void)initPreferences { [TPCApplicationInfo incrementApplicationRunCount]; // ====================================================== // [self registerDefaults]; [self _migrateWorldControllerToVersion600]; #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 [self _migrateSparkleConfigurationToVersion601]; #endif [self _migrateAppearanceToVersion7011]; [self _migrateNicknameColorOverridesToVersion722]; [TPCPathInfo startUsingTranscriptFolderURL]; [RZUserDefaults() addObserver:(id)self forKeyPath:@"Highlight List -> Excluded Matches" options:NSKeyValueObservingOptionNew context:NULL]; [RZUserDefaults() addObserver:(id)self forKeyPath:@"Highlight List -> Primary Matches" options:NSKeyValueObservingOptionNew context:NULL]; [self _loadExcludeKeywords]; [self _loadMatchKeywords]; } #pragma mark - #pragma mark NSTextView Preferences + (BOOL)textFieldAutomaticSpellCheck { return [RZUserDefaults() boolForKey:@"TextFieldAutomaticSpellCheck"]; } + (void)setTextFieldAutomaticSpellCheck:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldAutomaticSpellCheck"]; } + (BOOL)textFieldAutomaticGrammarCheck { return [RZUserDefaults() boolForKey:@"TextFieldAutomaticGrammarCheck"]; } + (void)setTextFieldAutomaticGrammarCheck:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldAutomaticGrammarCheck"]; } + (BOOL)textFieldAutomaticSpellCorrection { return [RZUserDefaults() boolForKey:@"TextFieldAutomaticSpellCorrection"]; } + (void)setTextFieldAutomaticSpellCorrection:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldAutomaticSpellCorrection"]; } + (BOOL)textFieldSmartCopyPaste { return [RZUserDefaults() boolForKey:@"TextFieldSmartCopyPaste"]; } + (void)setTextFieldSmartCopyPaste:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldSmartCopyPaste"]; } + (BOOL)textFieldSmartQuotes { return [RZUserDefaults() boolForKey:@"TextFieldSmartQuotes"]; } + (void)setTextFieldSmartQuotes:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldSmartQuotes"]; } + (BOOL)textFieldSmartDashes { return [RZUserDefaults() boolForKey:@"TextFieldSmartDashes"]; } + (void)setTextFieldSmartDashes:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldSmartDashes"]; } + (BOOL)textFieldSmartLinks { return [RZUserDefaults() boolForKey:@"TextFieldSmartLinks"]; } + (void)setTextFieldSmartLinks:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldSmartLinks"]; } + (BOOL)textFieldDataDetectors { return [RZUserDefaults() boolForKey:@"TextFieldDataDetectors"]; } + (void)setTextFieldDataDetectors:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldDataDetectors"]; } + (BOOL)textFieldTextReplacement { return [RZUserDefaults() boolForKey:@"TextFieldTextReplacement"]; } + (void)setTextFieldTextReplacement:(BOOL)value { [RZUserDefaults() setBool:value forKey:@"TextFieldTextReplacement"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/TPCPreferencesReload.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXAppearancePrivate.h" #import "NSObjectHelperPrivate.h" #import "TXMasterControllerPrivate.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCThemeControllerPrivate.h" #import "IRCClientPrivate.h" #import "IRCChannelPrivate.h" #import "IRCCommandIndexPrivate.h" #import "IRCWorld.h" #import "TLOEncryptionManagerPrivate.h" #import "TLOInputHistoryPrivate.h" #import "TVCDockIconPrivate.h" #import "TVCLogControllerPrivate.h" #import "TVCLogControllerHistoricLogFilePrivate.h" #import "TVCMainWindowPrivate.h" #import "TVCMainWindowTextViewPrivate.h" #import "TVCServerListPrivate.h" #import "TVCMemberListPrivate.h" #import "TVCMemberListAppearance.h" #import "TPCPreferencesReload.h" NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Public @implementation TPCPreferences (TPCPreferencesReload) + (void)performReloadActionForKeys:(NSArray *)keys { NSParameterAssert(keys != nil); TPCPreferencesReloadAction reloadAction = 0; /* Style specific reloads... */ if ([keys containsObject:@"AutomaticallyFilterUnicodeTextSpam"] || [keys containsObject:@"ConversationTrackingIncludesUserModeSymbol"] || [keys containsObject:@"DisableRemoteNicknameColorHashing"] || [keys containsObject:@"DisplayEventInLogView -> Date Changes"] || [keys containsObject:@"DisplayEventInLogView -> Inline Media"] || [keys containsObject:@"DisplayEventInLogView -> Join, Part, Quit"] || [keys containsObject:@"Theme -> Nickname Format"] || [keys containsObject:@"Theme -> Timestamp Format"] || [keys containsObject:@"Theme -> Channel Font Preference Enabled"] || [keys containsObject:@"Theme -> Nickname Format Preference Enabled"] || [keys containsObject:@"Theme -> Timestamp Format Preference Enabled"] || [keys containsObject:TPCPreferencesThemeFontNameDefaultsKey] || [keys containsObject:TPCPreferencesThemeFontSizeDefaultsKey] || [keys containsObject:TPCPreferencesThemeNameDefaultsKey]) { reloadAction |= TPCPreferencesReloadActionStyle; } /* Highlight lists */ if ([keys containsObject:@"Highlight List -> Excluded Matches"] || [keys containsObject:@"Highlight List -> Primary Matches"]) { reloadAction |= TPCPreferencesReloadActionHighlightKeywords; } /* Highlight logging */ if ([keys containsObject:@"LogHighlights"]) { reloadAction |= TPCPreferencesReloadActionHighlightLogging; } /* Text direction: right-to-left, left-to-right */ if ([keys containsObject:@"RightToLeftTextFormatting"]) { reloadAction |= TPCPreferencesReloadActionTextDirection; } /* Text field font size */ if ([keys containsObject:@"Main Input Text Field -> Font Size"]) { reloadAction |= TPCPreferencesReloadActionTextFieldFontSize; } /* Input history scope */ if ([keys containsObject:@"SaveInputHistoryPerSelection"]) { reloadAction |= TPCPreferencesReloadActionInputHistoryScope; } /* Main window segmented controller */ if ([keys containsObject:@"DisableMainWindowSegmentedController"]) { reloadAction |= TPCPreferencesReloadActionTextFieldSegmentedControllerOrigin; } /* Main window alpha level */ if ([keys containsObject:@"MainWindowTransparencyLevel"]) { reloadAction |= TPCPreferencesReloadActionMainWindowTransparencyLevel; } /* Dock icon */ if ([keys containsObject:@"DisplayDockBadges"] || [keys containsObject:@"DisplayPublicMessageCountInDockBadge"]) { reloadAction |= TPCPreferencesReloadActionDockIconBadges; } /* Main window appearance */ if ([keys containsObject:@"Appearance"]) { reloadAction |= TPCPreferencesReloadActionAppearance; } /* Member list sort order */ if ([keys containsObject:@"MemberListSortFavorsServerStaff"]) { reloadAction |= TPCPreferencesReloadActionMemberListSortOrder; } /* Member list user badge colors */ if ([keys containsObject:@"DisplayUserListNoModeSymbol"] || [keys containsObject:@"User List Mode Badge Colors -> +y"] || [keys containsObject:@"User List Mode Badge Colors -> +q"] || [keys containsObject:@"User List Mode Badge Colors -> +a"] || [keys containsObject:@"User List Mode Badge Colors -> +o"] || [keys containsObject:@"User List Mode Badge Colors -> +h"] || [keys containsObject:@"User List Mode Badge Colors -> +v"] || [keys containsObject:@"User List Mode Badge Colors -> no mode"]) { reloadAction |= TPCPreferencesReloadActionMemberList; reloadAction |= TPCPreferencesReloadActionMemberListUserBadges; } /* Server list unread count badge colors */ if ([keys containsObject:@"Server List Unread Message Count Badge Colors -> Highlight"]) { reloadAction |= TPCPreferencesReloadActionServerListUnreadBadges; } /* Sparkle framework update feed URL */ #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 if ([keys containsObject:@"ReceiveBetaUpdates"]) { reloadAction |= TPCPreferencesReloadActionSparkleFrameworkFeedURL; } #endif /* Developer mode */ if ([keys containsObject:@"TextualDeveloperEnvironment"]) { reloadAction |= TPCPreferencesReloadActionIRCCommandCache; } /* Scrollback limit */ if ([keys containsObject:@"ScrollbackMaximumSavedLineCount"]) { reloadAction |= TPCPreferencesReloadActionScrollbackSaveLimit; } if ([keys containsObject:@"ScrollbackMaximumVisibleLineCount"]) { reloadAction |= TPCPreferencesReloadActionScrollbackVisibleLimit; } /* Channel view arrangement */ if ([keys containsObject:@"ChannelViewArrangement"]) { reloadAction |= TPCPreferencesReloadActionChannelViewArrangement; } /* Encryption policy */ #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 if ([keys containsObject:@"Off-the-Record Messaging -> Enable Encryption"] || [keys containsObject:@"Off-the-Record Messaging -> Automatically Enable Service"] || [keys containsObject:@"Off-the-Record Messaging -> Require Encryption"]) { reloadAction |= TPCPreferencesReloadActionEncryptionPolicy; } #endif /* After this is all complete; we call -preferencesChanged just to take care of everything else that does not need specific reloads. */ reloadAction |= TPCPreferencesReloadActionPreferencesChanged; [self performReloadAction:reloadAction]; } + (void)performReloadAction:(TPCPreferencesReloadAction)reloadAction { [self performReloadAction:reloadAction forKey:nil]; } + (void)performReloadAction:(TPCPreferencesReloadAction)reloadAction forKey:(nullable NSString *)key { /* Update dock icon */ if ((reloadAction & TPCPreferencesReloadActionDockIconBadges) == TPCPreferencesReloadActionDockIconBadges) { [TVCDockIcon updateDockIcon]; } /* As reloading the theme will also reload the server and member list, we keep track of whether that happened so it is not performed more than one time. */ BOOL didReloadActiveStyle = NO; BOOL didReloadUserInterface = NO; /* Member list appearance */ if ((reloadAction & TPCPreferencesReloadActionMemberListUserBadges) == TPCPreferencesReloadActionMemberListUserBadges) { /* We invalidate this early because a separate action may which is attached to our mask may reload the drawings for us so until we know if that happened, we wait. */ /* If we know FOR CERTAIN we only are ONLY reloading the user badges and we have a key for context, then be more efficient by only updating drawings related to this preference. The member list automatically invalidates its caches when passing a recognized key. */ if (reloadAction == TPCPreferencesReloadActionMemberListUserBadges && key != nil) { [mainWindowMemberList() refreshDrawingForChangesToPreference:key]; } else { [mainWindowMemberList().userInterfaceObjects invalidateUserMarkBadgeCaches]; } } /* Window appearance */ if ((reloadAction & TPCPreferencesReloadActionAppearance) == TPCPreferencesReloadActionAppearance) { [[TXSharedApplication sharedAppearance] updateAppearance]; didReloadUserInterface = YES; } /* Active style */ if ((reloadAction & TPCPreferencesReloadActionStyle) == TPCPreferencesReloadActionStyle) { [themeController() reload]; [mainWindow() reloadTheme]; didReloadActiveStyle = YES; } /* Server list */ if ((reloadAction & TPCPreferencesReloadActionServerList) == TPCPreferencesReloadActionServerList) { if (didReloadUserInterface == NO) { [mainWindowServerList() applicationAppearanceChanged]; } } else if ((reloadAction & TPCPreferencesReloadActionServerListUnreadBadges) == TPCPreferencesReloadActionServerListUnreadBadges) { if (didReloadUserInterface == NO) { /* The color used for unread badges also apply to the text color so we must reload all drawings instead of only the badges themselves. */ [mainWindowServerList() refreshAllDrawings]; } } /* Member list appearance */ if ((reloadAction & TPCPreferencesReloadActionMemberList) == TPCPreferencesReloadActionMemberList) { if (didReloadUserInterface == NO) { [mainWindowMemberList() applicationAppearanceChanged]; } } /* Member list sort order */ BOOL didReloadMemberListSortOrder = NO; if ((reloadAction & TPCPreferencesReloadActionMemberListSortOrder) == TPCPreferencesReloadActionMemberListSortOrder) { for (IRCClient *u in worldController().clientList) { for (IRCChannel *c in u.channelList) { [c sortMembers]; } } didReloadMemberListSortOrder = YES; } /* Member list appearance */ if ((reloadAction & TPCPreferencesReloadActionMemberList) == TPCPreferencesReloadActionMemberList) { /* Sort order will redraw these for us */ if (didReloadMemberListSortOrder == NO) { [mainWindowMemberList() refreshAllDrawings]; } } /* Main window segmented controller */ if ((reloadAction & TPCPreferencesReloadActionTextFieldSegmentedControllerOrigin) == TPCPreferencesReloadActionTextFieldSegmentedControllerOrigin) { [mainWindowTextField() reloadOriginPointsAndRecalculateSize]; } /* Main window alpha level */ if ((reloadAction & TPCPreferencesReloadActionMainWindowTransparencyLevel) == TPCPreferencesReloadActionMainWindowTransparencyLevel) { [mainWindow() updateAlphaValueToReflectPreferences]; } /* Highlight keywords */ if ((reloadAction & TPCPreferencesReloadActionHighlightKeywords) == TPCPreferencesReloadActionHighlightKeywords) { [self cleanUpHighlightKeywords]; } /* Highlight logging */ if ((reloadAction & TPCPreferencesReloadActionHighlightLogging) == TPCPreferencesReloadActionHighlightLogging) { if ([self logHighlights] == NO) { for (IRCClient *u in worldController().clientList) { [u clearCachedHighlights]; } } } /* Text direction: right-to-left, left-to-right */ if ((reloadAction & TPCPreferencesReloadActionTextDirection) == TPCPreferencesReloadActionTextDirection) { [mainWindowTextField() updateTextDirection]; if (didReloadActiveStyle == NO) { [mainWindow() reloadTheme]; } } /* Text field font size */ if ((reloadAction & TPCPreferencesReloadActionTextFieldFontSize) == TPCPreferencesReloadActionTextFieldFontSize) { [mainWindowTextField() updateTextBasedOnPreferredFontSize]; } /* Input history scope */ if ((reloadAction & TPCPreferencesReloadActionInputHistoryScope) == TPCPreferencesReloadActionInputHistoryScope) { [mainWindow().inputHistoryManager noteInputHistoryObjectScopeDidChange]; } /* Sparkle framework update feed URL */ #if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1 if ((reloadAction & TPCPreferencesReloadActionSparkleFrameworkFeedURL) == TPCPreferencesReloadActionSparkleFrameworkFeedURL) { [masterController() prepareThirdPartyServiceSparkleFramework]; } #endif /* Command index cache */ if ((reloadAction & TPCPreferencesReloadActionIRCCommandCache) == TPCPreferencesReloadActionIRCCommandCache) { [IRCCommandIndex invalidateCaches]; } /* Transcript folder URL */ if ((reloadAction & TPCPreferencesReloadActionLogTranscripts) == TPCPreferencesReloadActionLogTranscripts) { for (IRCClient *u in worldController().clientList) { [u reopenLogFileIfNeeded]; for (IRCChannel *c in u.channelList) { [c reopenLogFileIfNeeded]; } } } /* Scrollback limit */ if ((reloadAction & TPCPreferencesReloadActionScrollbackSaveLimit) == TPCPreferencesReloadActionScrollbackSaveLimit) { [TVCLogControllerHistoricLogSharedInstance() resetMaximumLineCount]; } if ((reloadAction & TPCPreferencesReloadActionScrollbackVisibleLimit) == TPCPreferencesReloadActionScrollbackSaveLimit) { for (IRCClient *u in worldController().clientList) { [u.viewController changeScrollbackLimit]; for (IRCChannel *c in u.channelList) { [c.viewController changeScrollbackLimit]; } } } /* Channel view arrangement */ if ((reloadAction & TPCPreferencesReloadActionChannelViewArrangement) == TPCPreferencesReloadActionChannelViewArrangement) { [mainWindow() updateChannelViewArrangement]; } /* Encryption policy */ #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 if ((reloadAction & TPCPreferencesReloadActionEncryptionPolicy) == TPCPreferencesReloadActionEncryptionPolicy) { [sharedEncryptionManager() updatePolicy]; /* Maybe remove title bar accessory view if encryption is disabled. */ [mainWindow() updateTitle]; } #endif /* World controller preferences changed call */ if ((reloadAction & TPCPreferencesReloadActionPreferencesChanged) == TPCPreferencesReloadActionPreferencesChanged) { [worldController() preferencesChanged]; [mainWindow() preferencesChanged]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/TPCPreferencesUserDefaultsLocal.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCResourceManager.h" #import "TPCPreferencesUserDefaultsLocal.h" NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Reading & Writing typedef NS_ENUM(NSUInteger, TPCPreferencesComparator) { TPCPreferencesComparatorEqual = 0, TPCPreferencesComparatorAnchorFront = 1, TPCPreferencesComparatorAnchorBack = 2 }; @interface TPCPreferencesUserDefaults () - (void)_setObject:(nullable id)value forKey:(NSString *)defaultName; @end @implementation TPCPreferencesUserDefaults (TPCPreferencesUserDefaultsLocal) + (BOOL)key:(NSString *)defaultName1 matchesKey:(NSString *)defaultName2 usingComparator:(TPCPreferencesComparator)comparator { NSParameterAssert(defaultName1 != nil); NSParameterAssert(defaultName2 != nil); if (comparator == TPCPreferencesComparatorEqual) { if ([defaultName1 isEqualToString:defaultName2]) { return YES; } } else if (comparator == TPCPreferencesComparatorAnchorFront) { if ([defaultName1 hasPrefix:defaultName2]) { return YES; } } else if (comparator == TPCPreferencesComparatorAnchorBack) { if ([defaultName1 hasSuffix:defaultName2]) { return YES; } } return NO; } + (BOOL)keyIsExcludedFromExportImport:(NSString *)defaultName { NSParameterAssert(defaultName != nil); NSDictionary *cachedValues = [TPCResourceManager dictionaryFromResources:@"KeysExcludedFromExport" inDirectory:@"Preferences"]; __block BOOL returnValue = NO; [cachedValues enumerateKeysAndObjectsUsingBlock:^(NSString *cachedKey, NSNumber *cachedObject, BOOL *stop) { if ([self key:defaultName matchesKey:cachedKey usingComparator:cachedObject.unsignedIntegerValue]) { *stop = YES; returnValue = YES; } }]; if (returnValue) { return YES; } return ([self keyAppearsInMasterList:defaultName] == NO); } + (BOOL)keyIsExcludedFromMigration:(NSString *)defaultName { NSParameterAssert(defaultName != nil); NSDictionary *cachedValues = [TPCResourceManager dictionaryFromResources:@"KeysExcludedFromMigrate" inDirectory:@"Preferences"]; __block BOOL returnValue = NO; [cachedValues enumerateKeysAndObjectsUsingBlock:^(NSString *cachedKey, NSNumber *cachedObject, BOOL *stop) { if ([self key:defaultName matchesKey:cachedKey usingComparator:cachedObject.unsignedIntegerValue]) { *stop = YES; returnValue = YES; } }]; if (returnValue) { return YES; } return ([self keyAppearsInMasterList:defaultName] == NO); } + (BOOL)keyAppearsInMasterList:(NSString *)defaultName { NSDictionary *cachedValues = [TPCResourceManager dictionaryFromResources:@"PreferenceKeyMasterList" inDirectory:@"Preferences"]; __block BOOL returnValue = NO; [cachedValues enumerateKeysAndObjectsUsingBlock:^(NSString *cachedKey, NSNumber *cachedObject, BOOL *stop) { if ([self key:defaultName matchesKey:cachedKey usingComparator:cachedObject.unsignedIntegerValue]) { *stop = YES; returnValue = YES; } }]; return returnValue; } + (BOOL)keyIsExcludedFromContainer:(NSString *)defaultName { NSParameterAssert(defaultName != nil); NSDictionary *cachedValues = [TPCResourceManager dictionaryFromResources:@"KeysExcludedFromContainer" inDirectory:@"Preferences"]; __block BOOL returnValue = NO; [cachedValues enumerateKeysAndObjectsUsingBlock:^(NSString *cachedKey, NSNumber *cachedObject, BOOL *stop) { if ([self key:defaultName matchesKey:cachedKey usingComparator:cachedObject.unsignedIntegerValue]) { *stop = YES; returnValue = YES; } }]; return returnValue; } - (void)_migrateObject:(nullable id)value forKey:(NSString *)defaultName { if ([TPCPreferencesUserDefaults keyIsExcludedFromContainer:defaultName]) { [[NSUserDefaults standardUserDefaults] setObject:value forKey:defaultName]; return; } [self _setObject:value forKey:defaultName]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/TPCResourceManager.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCAlert.h" #import "TPCApplicationInfo.h" #import "TPCPathInfo.h" #import "TLOLocalization.h" #import "TPCResourceManagerPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TPCResourceManagerBundleDocumentTypeExtension = @".bundle"; NSString * const TPCResourceManagerBundleDocumentTypeExtensionWithoutPeriod = @"bundle"; NSString * const TPCResourceManagerScriptDocumentTypeExtension = @".scpt"; NSString * const TPCResourceManagerScriptDocumentTypeExtensionWithoutPeriod = @"scpt"; @implementation TPCResourceManager + (void)copyResourcesToApplicationSupportFolder { /* Add a system link for the unsupervised scripts folder if it exists. */ NSString *sourcePath = [TPCPathInfo customScripts]; NSString *destinationPath = [[TPCPathInfo groupContainerApplicationSupport] stringByAppendingPathComponent:@"/Custom Scripts/"]; if ([RZFileManager() fileExistsAtPath:sourcePath] && [RZFileManager() fileExistsAtPath:destinationPath] == NO) { [RZFileManager() createSymbolicLinkAtPath:destinationPath withDestinationPath:sourcePath error:NULL]; } } + (NSCache *)sharedResourcesCache { static NSCache *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = [NSCache new]; }); return cachedValue; } + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name { return [self dictionaryFromResources:name inDirectory:nil key:nil cacheValue:YES]; } + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath { return [self dictionaryFromResources:name inDirectory:subpath key:nil cacheValue:YES]; } + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath cacheValue:(BOOL)cacheValue { return [self dictionaryFromResources:name inDirectory:subpath key:nil cacheValue:cacheValue]; } + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name cacheValue:(BOOL)cacheValue { return [self dictionaryFromResources:name inDirectory:nil key:nil cacheValue:cacheValue]; } + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name key:(nullable NSString *)key { return [self dictionaryFromResources:name inDirectory:nil key:key cacheValue:YES]; } + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name key:(nullable NSString *)key cacheValue:(BOOL)cacheValue { return [self dictionaryFromResources:name inDirectory:nil key:key cacheValue:cacheValue]; } + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key { return [self dictionaryFromResources:name inDirectory:subpath key:key cacheValue:YES]; } + (nullable NSDictionary *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key cacheValue:(BOOL)cacheValue { return [self objectFromResources:name inDirectory:subpath key:key kindOf:[NSDictionary class] cacheValue:cacheValue]; } + (nullable NSArray *)arrayFromResources:(NSString *)name { return [self arrayFromResources:name inDirectory:nil key:nil cacheValue:YES]; } + (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath { return [self arrayFromResources:name inDirectory:subpath key:nil cacheValue:YES]; } + (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath cacheValue:(BOOL)cacheValue { return [self arrayFromResources:name inDirectory:subpath key:nil cacheValue:cacheValue]; } + (nullable NSArray *)arrayFromResources:(NSString *)name cacheValue:(BOOL)cacheValue { return [self arrayFromResources:name inDirectory:nil key:nil cacheValue:cacheValue]; } + (nullable NSArray *)arrayFromResources:(NSString *)name key:(nullable NSString *)key { return [self arrayFromResources:name inDirectory:nil key:key cacheValue:YES]; } + (nullable NSArray *)arrayFromResources:(NSString *)name key:(nullable NSString *)key cacheValue:(BOOL)cacheValue { return [self arrayFromResources:name inDirectory:nil key:key cacheValue:cacheValue]; } + (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key { return [self arrayFromResources:name inDirectory:subpath key:key cacheValue:YES]; } + (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key cacheValue:(BOOL)cacheValue { return [self objectFromResources:name inDirectory:subpath key:key kindOf:[NSArray class] cacheValue:cacheValue]; } + (nullable id)objectFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key kindOf:(Class)class cacheValue:(BOOL)cacheValue { NSParameterAssert(name != nil); /* Bypass cache? */ if (cacheValue == NO) { return [self _objectFromResources:name inDirectory:subpath key:key kindOf:class]; } /* Cache hit? */ NSString *cacheKey = [NSString stringWithFormat:@"%@.plist / %@ / %@", name, ((subpath) ?: @"Root Folder"), ((key) ?: @"Root Object")]; NSCache *cache = self.sharedResourcesCache; id cachedValue = [cache objectForKey:cacheKey]; if (cachedValue) { return cachedValue; } /* No cache hit, generate */ cachedValue = [self _objectFromResources:name inDirectory:subpath key:key kindOf:class]; if (cachedValue) { [cache setObject:cachedValue forKey:cacheKey]; } return cachedValue; } + (nullable id)_objectFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key kindOf:(Class)class { /* Locate resource */ NSURL *resourceURL = [RZMainBundle() URLForResource:name withExtension:@"plist" subdirectory:subpath]; if (resourceURL == nil) { LogToConsoleError("Resource '%{public}@' in subpath '%{public}@' was not found.", name, ((subpath) ?: @"")); return nil; } /* Read resource */ NSError *readError = nil; NSData *fileContents = [NSData dataWithContentsOfURL:resourceURL options:0 error:&readError]; if (readError) { LogToConsoleError("Resource '%{public}@' could not be read with error: %{public}@", resourceURL.standardizedTildePath, readError.localizedDescription); return nil; } /* Process as a property list */ NSError *parseError = nil; id propertyList = [NSPropertyListSerialization propertyListWithData:fileContents options:NSPropertyListImmutable format:NULL error:&parseError]; if (parseError) { LogToConsoleFault("Resource '%{public}@' could not be parsed as a property list with error: %{public}@", resourceURL.standardizedTildePath, parseError.localizedDescription); return nil; } /* Locate object */ /* Until we know otherwise, the value is the root object. */ id objectValue = nil; if (key == nil) { objectValue = propertyList; } else { /* Property list can be an array. */ if ([propertyList isKindOfClass:[NSDictionary class]] == NO) { LogToConsoleError("Contents of resource '%{public}@' is not a dictionary. " "Cannot locate value of 'key' in other formats.", resourceURL.standardizedTildePath); return nil; } objectValue = [propertyList objectForKey:key]; } if ([objectValue isKindOfClass:class] == NO) { LogToConsoleError("Contents of key '%{public}@' in resource '%{public}@' is not kind of class: %{public}@", ((key) ?: @""), resourceURL.standardizedTildePath, NSStringFromClass(class)); return nil; } return objectValue; } @end #pragma mark - @implementation TPCResourceManagerDocumentTypeImporter + (BOOL)autosavesInPlace { /* We are read-only. This suppresses a warning in console. */ return YES; } - (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError { NSString *filePath = url.filePathURL.absoluteString; if ([filePath hasSuffix:TPCResourceManagerScriptDocumentTypeExtension]) { [self performImportOfScriptFile:url]; return YES; } NSString *pluginSuffix = [TPCResourceManagerBundleDocumentTypeExtension stringByAppendingString:@"/"]; if ([filePath hasSuffix:pluginSuffix]) { [self performImportOfPluginFile:url]; return YES; } return NO; } #pragma mark - #pragma mark Custom Plugin Files - (void)performImportOfPluginFile:(NSURL *)url { NSParameterAssert(url != nil); NSString *filename = url.lastPathComponent; BOOL performInstall = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[6tj-yp]") title:TXTLS(@"Prompts[xfl-8e]", filename) defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]")]; if (performInstall == NO) { return; } NSURL *newPath = [[TPCPathInfo customExtensionsURL] URLByAppendingPathComponent:filename]; BOOL didImport = [self import:url into:newPath]; if (didImport) { NSString *filenameWithoutExtension = filename.stringByDeletingPathExtension; [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[k69-q0]") title:TXTLS(@"Prompts[xek-0t]", filenameWithoutExtension) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } } #pragma mark - #pragma mark Custom Script Files - (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError **)outError { NSString *scriptsPath = [TPCPathInfo customScripts]; if ([url.path hasPrefix:scriptsPath] == NO) { if (outError) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[NSURLErrorKey] = url; userInfo[NSLocalizedDescriptionKey] = TXTLS(@"Prompts[m2r-gv]"); userInfo[NSLocalizedRecoverySuggestionErrorKey] = TXTLS(@"Prompts[ztu-nv]"); *outError = [NSError errorWithDomain:TXErrorDomain code:27984 userInfo:userInfo]; } return NO; } return YES; } - (void)performImportOfScriptFile:(NSURL *)url { NSParameterAssert(url != nil); NSString *filename = url.lastPathComponent; /* Ask user before installing. */ BOOL performInstall = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[6tj-yp]") title:TXTLS(@"Prompts[xfl-8e]", filename) defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]")]; if (performInstall == NO) { return; // Do not install. } #if TEXTUAL_BUILT_INSIDE_SANDBOX == 0 NSURL *newPath = [[TPCPathInfo customScriptsURL] URLByAppendingPathComponent:filename]; BOOL didImport = [self import:url into:newPath]; if (didImport) { [self performImportOfScriptFilePostflight:filename]; } #else NSURL *folderRep = [TPCPathInfo customScriptsURL]; if ([RZFileManager() fileExistsAtURL:folderRep] == NO) { folderRep = [TPCPathInfo userApplicationScriptsURL]; } NSString *bundleID = [TPCApplicationInfo applicationBundleIdentifier]; NSSavePanel *d = [NSSavePanel savePanel]; d.delegate = (id)self; d.canCreateDirectories = YES; d.directoryURL = folderRep; d.title = TXTLS(@"Prompts[6hx-ni]"); d.message = TXTLS(@"Prompts[0bj-ic]", bundleID); d.nameFieldStringValue = url.lastPathComponent; d.showsTagField = NO; [d beginWithCompletionHandler:^(NSInteger returnCode) { if (returnCode == NSModalResponseOK) { if ([self import:url into:d.URL] == NO) { return; } NSString *filename = d.URL.lastPathComponent; XRPerformBlockAsynchronouslyOnMainQueue(^{ [self performImportOfScriptFilePostflight:filename]; }); } }]; #endif } - (void)performImportOfScriptFilePostflight:(NSString *)filename { NSParameterAssert(filename != nil); NSString *filenameWithoutExtension = filename.stringByDeletingPathExtension; [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[3ze-xh]", filenameWithoutExtension) title:TXTLS(@"Prompts[4ua-v5]", filenameWithoutExtension) defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } #pragma mark - #pragma mark General Import Controller - (BOOL)import:(NSURL *)url into:(NSURL *)destination { return [RZFileManager() replaceItemAtURL:destination withItemAtURL:url options:(CSFileManagerOptionsMoveToTrash | CSFileManagerOptionsRemoveIfExists)]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/TPCSandboxMigration.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2024 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TDCAlert.h" #import "TLOLocalization.h" #import "TLOpenLink.h" #import "TPCApplicationInfo.h" #import "TPCPathInfo.h" #import "TPCPreferencesUserDefaults.h" #import "TPCPreferencesUserDefaultsLocal.h" #import "TPCSandboxMigrationPrivate.h" NS_ASSUME_NONNULL_BEGIN /* Standalone Classic - This refers to an installation of Textual 7 downloaded from codeux.com before the transition to sandbox. Mac App Store - This refers to an installation of Textual 7 downloaded from the Mac App Store. Migration steps: 1. Check the age of the current group container. If it wasn't recently created. Like within the span of a few seconds ago, then stop. 2. Locate Standalone Classic preferences 3. If located, is the value of TXRunCount >= 1 If yes, perform migration on that installation. 4. If migration is not performed, then locate Mac App Store preferences if this is the first time this installation of Textual has launched. 5. If located, is the value of TXRunCount >= 1 If yes, have the preferences been modified with the last 30 days (see MaximumAgeOfStalePreferences) If yes, perform migration on that installation. Data and files are migrated by creating copies. After migration is completed, the user will be asked if they want to remove (delete / erase) the old contents. */ /* Textual will import preferences from a Mac App Store purchase if this is the first launch. This macro defines the maximum amount of time that was allowed to elapse since the preferences for that installation was last modified. We don't want to import an installation that the user setup a year ago on a whim and only used it a day. */ #define MaximumAgeOfStalePreferences (60 * 60 * 24 * 30) // 30 days /* Defaults key set after migration is performed. */ /* YES if migration was completed */ #define MigrationCompleteDefaultsKey @"Sandbox Migration -> Migrated Resources" /* Integer for the installation that was migrated */ /* nil value or zero result is possible even after migration is complete because nothing might have been migrated. */ /* Migration is designed to be one shot. */ #define MigrationInstallationMigratedDefaultsKey @"Sandbox Migration -> Installation Migrated" /* Whether the user has dismissed the notification alert */ #define MigrationUserAcknowledgedDefaultsKey @"Sandbox Migration -> User Acknowledged" /* Whether the user wants to delete old files */ #define MigrationUserPrefersPruningDefaultsKey @"Sandbox Migration -> User Prefers Pruning Files" /* YES if there are no more extensions to prune which means we can bypass all the directory scans. */ #define MigrationAllExtensionsPrunedDefaultsKey @"Sandbox Migration -> All Extensions Pruned" /* An array of preference keys migrated */ #define MigrationKeysImportedDefaultsKey @"Sandbox Migration -> Imported Keys" /* Result returned when performing migration of a specific installation. */ typedef NS_ENUM(NSUInteger, TPCMigrateSandboxResult) { /* Migration was performed successfully */ TPCMigrateSandboxResultSuccess, /* Candidate for migration is not suitable. For example, a Mac App Store installation was located but the age of its preference file is out of range. */ TPCMigrateSandboxResultNotSuitable, /* An error occurred during migration. */ /* Errors are logged to console. */ TPCMigrateSandboxResultError }; /* The installation attempting migration / migrated */ typedef NS_ENUM(NSUInteger, TPCMigrateSandboxInstallation) { /* Standalone Classic */ TPCMigrateSandboxInstallationStandaloneClassic = 100, /* Mac App Store */ TPCMigrateSandboxInstallationMacAppStore = 200 }; @interface TPCPathInfo (TPCSandboxMigration) + (nullable NSURL *)_groupContainerURLForInstallation:(TPCMigrateSandboxInstallation)installation; + (nullable NSURL *)_groupContainerPreferencesURLForInstallation:(TPCMigrateSandboxInstallation)installation; + (nullable NSURL *)_groupContainerExtensionsURLForInstallation:(TPCMigrateSandboxInstallation)installation; @end @interface TPCResourceManager (TPCSandoxMigration) + (nullable NSArray *)_listOfExtensionsForInstallation:(TPCMigrateSandboxInstallation)installation; + (BOOL)_ageOfCurrentContainerIsRecent; + (NSTimeInterval)_modificationDateForMacAppStorePreferencesIsRecent; + (NSTimeInterval)_intervalSinceCreatedForURL:(NSURL *)url; + (NSTimeInterval)_intervalSinceLastModificationForURL:(NSURL *)url; + (BOOL)_URLIsSymbolicLink:(NSURL *)url; @end @interface TPCPreferencesUserDefaults () - (void)_migrateObject:(nullable id)value forKey:(NSString *)defaultName; @end @implementation TPCSandboxMigration + (void)migrateResources { LogToConsole("Preparing to migrate group containers"); /* Do not migrate if we have done so in the past. */ if ([RZUserDefaults() boolForKey:MigrationCompleteDefaultsKey]) { [self _notifyGroupContainerMigratedFromDefaults]; [self _pruneExtensionSymbolicLinksFromDefaults]; LogToConsole("Group containers have already been migrated"); return; } /* Do not migrate if the age of the current group container is not recent. The age is only going to be recent the launch it was created. */ if ([TPCResourceManager _ageOfCurrentContainerIsRecent] == NO) { [self _setMigrationCompleteAndAcknowledged]; LogToConsole("Current group container was not created recently"); return; } /* The order of this array is the order of preference for migration. If one migrates, then it stops there. */ /* I acknowledge creating a list of numbers just to enumerate them is not efficient. This will be ran once. It's okay. :) */ NSArray *installations = @[@(TPCMigrateSandboxInstallationStandaloneClassic), @(TPCMigrateSandboxInstallationMacAppStore)]; for (NSNumber *installationRef in installations) { if ([self _migrateInstallationEntry:installationRef]) { return; // Success } } /* No other migration path */ [self _setMigrationCompleteAndAcknowledged]; } #pragma mark - #pragma mark Standalone Classic Migration + (BOOL)_migrateInstallationEntry:(NSNumber *)installationRef { NSParameterAssert(installationRef != nil); TPCMigrateSandboxInstallation installation = installationRef.unsignedIntegerValue; NSString *description = [self _descriptionOfInstallation:installation]; LogToConsole("Start: Migrating [%{public}@] installation", description); TPCMigrateSandboxResult result = [self _migrateInstallation:installation]; switch (result) { case TPCMigrateSandboxResultSuccess: [self _setMigrationCompleteForInstallation:installation]; LogToConsole("End: Migrating [%{public}@] successful", description); return YES; // Stop further migration case TPCMigrateSandboxResultError: LogToConsole("End: Migrating [%{public}@] failed. Stopping all migration", description); return YES; // Stop further migration case TPCMigrateSandboxResultNotSuitable: LogToConsole("End: Migrating [%{public}@] failed. Installation is not suitable", description); return NO; // Allow further migration } } + (TPCMigrateSandboxResult)_migrateInstallation:(TPCMigrateSandboxInstallation)installation { /* Preflight checks */ BOOL isMacAppStore = (installation == TPCMigrateSandboxInstallationMacAppStore); if (isMacAppStore && [TPCResourceManager _modificationDateForMacAppStorePreferencesIsRecent] == NO) { LogToConsoleDebug("Migration of Mac App Store has stale preferences file"); return TPCMigrateSandboxResultNotSuitable; } NSString *suiteName = [self _defaultsSuiteNameForInstallation:installation]; NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; if (defaults == nil) { LogToConsole("NSUserDefaults object could not be created for [%{public}@] domain: '%{public}@'", [self _descriptionOfInstallation:installation], ((suiteName) ?: @"")); return TPCMigrateSandboxResultNotSuitable; } /* Import preference keys */ /* Import preferences before migrating group container that way if a hard failure is encountered there, it wont undo the progress we made. The user will want something rather than nothing. Especially when it comes to their configuration. Custom content can be copied manually. */ NSDictionary *preferences = defaults.dictionaryRepresentation; NSUInteger runCount = [preferences unsignedIntegerForKey:@"TXRunCount"]; if (runCount == 0) { LogToConsoleError("Migration of [%{public}@] has zero run count", [self _descriptionOfInstallation:installation]); return TPCMigrateSandboxResultNotSuitable; } /* Import preferences */ NSArray *importedKeys = [self _importPreferences:preferences]; /* Migrate group container */ BOOL migrateContainer = [self _migrateGroupContainerContentsForInstallation:installation]; if (migrateContainer == NO) { return TPCMigrateSandboxResultError; } /* Finish */ [self _setListOfImportedKeys:importedKeys]; [self _notifyGroupContainerMigratedForInstallation:installation]; return TPCMigrateSandboxResultSuccess; } + (NSArray *)_importPreferences:(NSDictionary *)dict { NSParameterAssert(dict != nil); LogToConsole("Start: Migrating preferences"); NSMutableArray *importedKeys = [NSMutableArray arrayWithCapacity:dict.count]; [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id object, BOOL *stop) { if ([TPCPreferencesUserDefaults keyIsExcludedFromMigration:key]) { #ifdef DEBUG LogToConsoleDebug("Key is excluded from migration: '%{public}@'", key); #endif return; } [importedKeys addObject:key]; [RZUserDefaults() _migrateObject:object forKey:key]; }]; LogToConsole("End: Migrating preferences"); return [importedKeys copy]; } + (void)_removeImportedKeysForInstallation:(TPCMigrateSandboxInstallation)installation { LogToConsole("Start: Remove old preferences"); NSArray *listOfKeys = [RZUserDefaults() arrayForKey:MigrationKeysImportedDefaultsKey]; if (listOfKeys == nil) { LogToConsole("No preferences to remove"); return; } NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:[self _defaultsSuiteNameForInstallation:installation]]; if (defaults == nil) { LogToConsole("NSUserDefaults object could not be created for [%{public}@] installation", [self _descriptionOfInstallation:installation]); return; } for (id key in listOfKeys) { /* This data could have been manipulated from the outside. */ if ([key isKindOfClass:[NSString class]] == NO) { LogToConsoleFault("Corrupted data found inside list of keys"); continue; } #ifdef DEBUG LogToConsoleDebug("Removing key: '%{public}@'", key); #endif [defaults removeObjectForKey:key]; } [self _unsetListOfImportedKeys]; LogToConsole("End: Remove old preferences - Removed: %{public}lu", listOfKeys.count); } + (void)_setListOfImportedKeys:(nullable NSArray *)list { [RZUserDefaults() _migrateObject:list forKey:MigrationKeysImportedDefaultsKey]; } + (void)_unsetListOfImportedKeys { [self _setListOfImportedKeys:nil]; } + (void)_setMigrationComplete { [RZUserDefaults() _migrateObject:@(YES) forKey:MigrationCompleteDefaultsKey]; } + (void)_setMigrationCompleteAndAcknowledged { /* This method is called when there was nothing to migrate so we set complete and pretend user acknowledged it. */ [self _setMigrationComplete]; [self _setUserAcknowledgedMigration]; } + (void)_setMigrationCompleteForInstallation:(TPCMigrateSandboxInstallation)installation { [RZUserDefaults() _migrateObject:@(installation) forKey:MigrationInstallationMigratedDefaultsKey]; [self _setMigrationComplete]; } #pragma mark - #pragma mark Group Container Migration + (BOOL)_migrateGroupContainerContentsForInstallation:(TPCMigrateSandboxInstallation)installation { LogToConsole("Start: Migrate group container for '%{public}@'", [self _descriptionOfInstallation:installation]); NSURL *oldLocation = [TPCPathInfo _groupContainerURLForInstallation:installation]; if (oldLocation == nil) { LogToConsoleError("Cannot migrate group container contents because of nil source location"); return NO; } NSURL *newLocation = [TPCPathInfo groupContainerURL]; if (newLocation == nil) { LogToConsoleError("Cannot migrate group container contents because of nil destination location"); return NO; } /* Migration is performed by recursively copying contents effectively merging the contents. */ /* If a file already exists, it will not be overwrote under any condition. */ /* This should only be performed with no real resources in the group container destination group container anyways so this wont matter much. */ BOOL result = [RZFileManager() mergeDirectoryAtURL:oldLocation withDirectoryAtURL:newLocation options:(CSFileManagerOptionsEnumerateDirectories | CSFileManagerOptionContinueOnError | CSFileManagerOptionsCreateDirectory | CSFileManagerCreateSymbolicLinkForPackages)]; LogToConsole("End: Migrate group container - Result: %{BOOL}d", result); return result; } + (void)_notifyGroupContainerMigratedForInstallation:(TPCMigrateSandboxInstallation)installation { LogToConsole("Notifying user that installation of type [%{public}@] migration performed", [self _descriptionOfInstallation:installation]); TVCAlert *alert = [TDCAlert alertWithMessage:TXTLS(@"Prompts[qy4-5o]") title:TXTLS(@"Prompts[ios-na]") defaultButton:TXTLS(@"Prompts[zjw-bd]") alternateButton:nil otherButton:TXTLS(@"Prompts[d90-au]") suppressionKey:nil suppressionText:TXTLS(@"Prompts[q3t-45]") completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id _Nullable underlyingAlert) { [self _setUserAcknowledgedMigration]; if (suppressed) { [self _setUserPrefersPruningFiles]; [self _removeImportedKeysForInstallation:installation]; [self _removeGroupContainerContentsForInstallation:installation]; } else { [self _unsetListOfImportedKeys]; } }]; [alert setButtonClickedBlock:^BOOL(TVCAlert *sender, TVCAlertResponseButton buttonClicked) { [TLOpenLink openWithString:@"https://help.codeux.com/textual/miscellaneous/Why-Did-Textual-Copy-Files-to-a-New-Location.kb" inBackground:NO]; return NO; } forButton:TVCAlertResponseButtonThird]; } + (void)_notifyGroupContainerMigratedFromDefaults { /* This method is only invoked internally if the defaults key MigrationCompleteDefaultsKey is set which means we are just asking the user what they want to do with their files until they acknowledge the alert. */ BOOL userAcknowledged = [RZUserDefaults() boolForKey:MigrationUserAcknowledgedDefaultsKey]; if (userAcknowledged) { return; // Stop migration } TPCMigrateSandboxInstallation installation = [RZUserDefaults() unsignedIntegerForKey:MigrationInstallationMigratedDefaultsKey]; if ([self _isInstallationSupported:installation] == NO) { [self _setUserAcknowledgedMigration]; return; // Stop migration } [self _notifyGroupContainerMigratedForInstallation:installation]; } + (void)_setUserAcknowledgedMigration { [RZUserDefaults() _migrateObject:@(YES) forKey:MigrationUserAcknowledgedDefaultsKey]; } + (void)_setUserPrefersPruningFiles { [RZUserDefaults() _migrateObject:@(YES) forKey:MigrationUserPrefersPruningDefaultsKey]; } #pragma mark - #pragma mark Group Container Removal + (BOOL)_removeGroupContainerContentsForInstallation:(TPCMigrateSandboxInstallation)installation { LogToConsole("Start: Remove group container for '%{public}@'", [self _descriptionOfInstallation:installation]); NSURL *gcLocation = [TPCPathInfo _groupContainerURLForInstallation:installation]; if (gcLocation == nil) { LogToConsoleError("Cannot remove group container contents because of nil location"); return NO; } /* -_listOfExtensionsForInstallation: should only return nil on fatal errors. It will not return nil for an extension folder that does not exist, or is empty. */ NSArray *oldExtensions = [TPCResourceManager _listOfExtensionsForInstallation:installation]; if (gcLocation == nil) { LogToConsoleError("Cannot remove group container contents because of nil extension list"); return NO; } LogToConsole("Removing group container contents at URL: %{public}@", gcLocation.standardizedTildePath); BOOL result = [RZFileManager() removeContentsOfDirectoryAtURL:gcLocation excludingURLs:oldExtensions options:(CSFileManagerOptionContinueOnError)]; LogToConsole("End: Remove group container - Result: %{BOOL}d", result); return result; } #pragma mark - #pragma mark Extension Pruning + (void)_pruneExtensionSymbolicLinksForInstallation:(TPCMigrateSandboxInstallation)installation { /* When you create a copy of a bundle programmatically, macOS will move it to quarantine. The user is then notified macOS can't verify that it isn't malware. That is less than ideal when performing migration. */ /* When migration is performed, we create a symbolic link to the original extension instead of copying it to the new location. If the symbolic link at the new location is replaced, then that extension is now just dangling never to be used again. */ /* TPCResourceManager will handle garbage collection. It will compare the contents of the old location and new location each launch. If a symbolic link is not present for an extension in the old location, that extension is deleted. Once all extensions are deleted, a flag is set to stop pruning so we don't keep scanning the old location forever. */ LogToConsole("Start: Pruning extensions for '%{public}@'", [self _descriptionOfInstallation:installation]); NSArray *oldExtensions = [TPCResourceManager _listOfExtensionsForInstallation:installation]; if (oldExtensions == nil) { /* Helper method will describe error. */ return; } if (oldExtensions.count == 0) { LogToConsole("Source location for extensions to prune is empty"); [self _setAllExtensionSymbolicLinksPruned]; return; } NSURL *newLocation = [TPCPathInfo customExtensionsURL]; if (newLocation == nil) { LogToConsoleError("Cannot prune extensions because of nil destination location"); return; } NSUInteger numberPruned = 0; NSUInteger numberRemaining = 0; for (NSURL *oldExtension in oldExtensions) { NSString *name = [oldExtension resourceValueForKey:NSURLNameKey]; NSNumber *isPackage = [oldExtension resourceValueForKey:NSURLIsPackageKey]; if ([name hasSuffix:@".bundle"] == NO || (isPackage == nil || isPackage.boolValue == NO)) { #ifdef DEBUG LogToConsoleDebug("Ignoring non-bundle: '%{public}@' - isPackage: %{BOOL}d", name, isPackage.boolValue); #endif continue; } NSURL *newExtension = [newLocation URLByAppendingPathComponent:name]; /* Should we check if the symbolic link points to this extension and not some other random file on the operating system? The likelihood of the user having a symbolic link they created is near zero if not zero. This is already over engineered. */ BOOL pruned = NO; if ([TPCResourceManager _URLIsSymbolicLink:newExtension] == NO) { #ifdef DEBUG LogToConsoleDebug("Pruning URL: '%{public}@'", oldExtension.standardizedTildePath); #endif NSError *deleteError = nil; pruned = [RZFileManager() removeItemAtURL:oldExtension error:&deleteError]; if (deleteError) { LogToConsoleError("Failed to prune extension at URL ['%{public}@']: %{public}@", oldExtension.standardizedTildePath, deleteError.localizedDescription); } } if (pruned) { numberPruned += 1; } else { numberRemaining += 1; } } // Directory list for loop if (numberRemaining == 0) { [self _setAllExtensionSymbolicLinksPruned]; } LogToConsole("End: Pruning extensions completed. " "Number remaining: %{public}lu, Number pruned: %{public}lu", numberRemaining, numberPruned); } + (void)_pruneExtensionSymbolicLinksFromDefaults { BOOL doPrune = [RZUserDefaults() boolForKey:MigrationUserPrefersPruningDefaultsKey]; if (doPrune == NO) { return; // Stop pruning } BOOL pruningExhausted = [RZUserDefaults() boolForKey:MigrationAllExtensionsPrunedDefaultsKey]; if (pruningExhausted) { return; // Stop pruning } TPCMigrateSandboxInstallation installation = [RZUserDefaults() unsignedIntegerForKey:MigrationInstallationMigratedDefaultsKey]; if ([self _isInstallationSupported:installation] == NO) { [self _setAllExtensionSymbolicLinksPruned]; return; // Stop pruning } [self _pruneExtensionSymbolicLinksForInstallation:installation]; } + (void)_setAllExtensionSymbolicLinksPruned { [RZUserDefaults() _migrateObject:@(YES) forKey:MigrationAllExtensionsPrunedDefaultsKey]; } #pragma mark - #pragma mark Utilities + (NSString *)_descriptionOfInstallation:(TPCMigrateSandboxInstallation)installation { /* This is used for logging so is not localized. Localize is use changes. */ switch (installation) { case TPCMigrateSandboxInstallationStandaloneClassic: return @"Standalone Classic"; case TPCMigrateSandboxInstallationMacAppStore: return @"Mac App Store"; default: return @""; } } + (nullable NSString *)_groupContainerIdentifierForInstallation:(TPCMigrateSandboxInstallation)installation { switch (installation) { case TPCMigrateSandboxInstallationStandaloneClassic: return @"com.codeux.apps.textual"; case TPCMigrateSandboxInstallationMacAppStore: return @"8482Q6EPL6.com.codeux.irc.textual"; default: break; } return nil; } + (nullable NSString *)_defaultsSuiteNameForInstallation:(TPCMigrateSandboxInstallation)installation { switch (installation) { case TPCMigrateSandboxInstallationMacAppStore: return @"8482Q6EPL6.com.codeux.irc.textual"; default: break; } return nil; } + (BOOL)_isInstallationSupported:(TPCMigrateSandboxInstallation)installation { return (installation == TPCMigrateSandboxInstallationStandaloneClassic || installation == TPCMigrateSandboxInstallationMacAppStore); } @end #pragma mark - #pragma mark Resource Management @implementation TPCResourceManager (TPCSandoxMigration) + (nullable NSArray *)_listOfExtensionsForInstallation:(TPCMigrateSandboxInstallation)installation { NSURL *oldLocation = [TPCPathInfo _groupContainerExtensionsURLForInstallation:installation]; if (oldLocation == nil) { LogToConsoleError("Cannot list extensions because of nil source location"); return nil; } if ([RZFileManager() fileExistsAtURL:oldLocation] == NO) { return @[]; } NSError *listExtensionsError = nil; NSArray *oldExtensions = [RZFileManager() contentsOfDirectoryAtURL:oldLocation includingPropertiesForKeys:@[NSURLNameKey, NSURLIsPackageKey] options:0 error:&listExtensionsError]; if (listExtensionsError) { LogToConsoleError("Unable to list contents of extensions at URL ['%{public}@']: %{public}@", oldLocation.standardizedTildePath, listExtensionsError.localizedDescription); return nil; } return oldExtensions; } + (BOOL)_ageOfCurrentContainerIsRecent { NSURL *newLocation = [TPCPathInfo groupContainerURL]; if (newLocation == NO) { return NO; } NSTimeInterval age = [self _intervalSinceCreatedForURL:newLocation]; /* macOS will create the group container the first time we ask for its path. If the group container wasn't created recently, then we have no reason to perform migration to it. In theory, this could probably be narrowed down further as the interval should be sub-second. */ return (age < 5.0); } + (NSTimeInterval)_modificationDateForMacAppStorePreferencesIsRecent { NSURL *location = [TPCPathInfo _groupContainerPreferencesURLForInstallation:TPCMigrateSandboxInstallationMacAppStore]; if (location == nil) { return NO; } NSTimeInterval age = [self _intervalSinceLastModificationForURL:location]; return (age >= 0 && age <= MaximumAgeOfStalePreferences); } + (NSTimeInterval)_intervalSinceCreatedForURL:(NSURL *)url { NSParameterAssert(url != nil); NSError *error = nil; NSTimeInterval age = [url intervalSinceCreatedWithError:&error]; if (error) { /* This is purposely considered debug information as the user knowing a file not existing is not an error when that is probable outcome. */ LogToConsoleDebug("Error caught when calculating age of file: %{public}@", error.localizedDescription); } return age; } + (NSTimeInterval)_intervalSinceLastModificationForURL:(NSURL *)url { NSParameterAssert(url != nil); NSError *error = nil; NSTimeInterval age = [url intervalSinceLastModificationWithError:&error]; if (error) { /* This is purposely considered debug information as the user knowing a file not existing is not an error when that is probable outcome. */ LogToConsoleDebug("Error caught when calculating age of file: %{public}@", error.localizedDescription); } return age; } + (BOOL)_URLIsSymbolicLink:(NSURL *)url { NSParameterAssert(url != nil); /* Skip check if file exists because no resource value will be returned if that is the case. */ NSNumber *isSymblink = [url resourceValueForKey:NSURLIsSymbolicLinkKey]; return (isSymblink != nil && isSymblink.boolValue); } @end #pragma mark - #pragma mark Path Information @implementation TPCPathInfo (TPCSandboxMigration) + (nullable NSURL *)_groupContainerURLForInstallation:(TPCMigrateSandboxInstallation)installation { NSString *identifier = [TPCSandboxMigration _groupContainerIdentifierForInstallation:installation]; if (identifier == nil) { return nil; } /* The reason we are not using -containerURLForSecurityApplicationGroupIdentifier: in this context is because during testing, that method was returning ~/Library/Containers/com.codeux.apps.textual instead of the group container location. I assume it's related to the fact the group identifier is same as the app's identifier. This is not a make-or-break location in which hard coding will hurt it. */ identifier = [NSString localizedStringWithFormat:@"/Library/Group Containers/%@/", identifier]; NSURL *baseURL = [[TPCPathInfo userHomeURL] URLByAppendingPathComponent:identifier]; return baseURL; } + (nullable NSURL *)_groupContainerPreferencesURLForInstallation:(TPCMigrateSandboxInstallation)installation { NSString *identifier = [TPCSandboxMigration _groupContainerIdentifierForInstallation:installation]; if (identifier == nil) { return nil; } identifier = [NSString localizedStringWithFormat:@"/Library/Group Containers/%1$@/Library/Preferences/%1$@.plist", identifier]; NSURL *baseURL = [[TPCPathInfo userHomeURL] URLByAppendingPathComponent:identifier]; return baseURL; } + (nullable NSURL *)_groupContainerExtensionsURLForInstallation:(TPCMigrateSandboxInstallation)installation { NSURL *sourceURL = [self _groupContainerURLForInstallation:installation]; if (sourceURL == nil) { return nil; } return [sourceURL URLByAppendingPathComponent:@"/Library/Application Support/Textual/Extensions/"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/Themes/TPCTheme.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXAppearance.h" #import "TPCApplicationInfo.h" #import "TPCPathInfo.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TPCResourceManager.h" #import "TPCThemePrivate.h" NS_ASSUME_NONNULL_BEGIN #define _templateEngineVersionMaximum TPCThemeSettingsNewestTemplateEngineVersion #define _templateEngineVersionMinimum TPCThemeSettingsNewestTemplateEngineVersion NSString * const TPCThemeIntegrityCompromisedNotification = @"TPCThemeIntegrityCompromisedNotification"; NSString * const TPCThemeIntegrityRestoredNotification = @"TPCThemeIntegrityRestoredNotification"; NSString * const TPCThemeAppearanceChangedNotification = @"TPCThemeAppearanceChangedNotification"; NSString * const TPCThemeVarietyChangedNotification = @"TPCThemeVarietyChangedNotification"; NSString * const TPCThemeWasModifiedNotification = @"TPCThemeWasModifiedNotification"; NSString * const TPCThemeWasDeletedNotification = @"TPCThemeWasDeletedNotification"; typedef NS_ENUM(NSUInteger, _TPCThemeChooseVarietyResult) { _TPCThemeChooseVarietyResultNoChange, _TPCThemeChooseVarietyResultNoBestChoice, _TPCThemeChooseVarietyResultChanged }; typedef NS_OPTIONS(NSUInteger, _TPCThemeMonitoringResult) { _TPCThemeMonitoringResultNoChange = 0, // Default _TPCThemeMonitoringResultReloadableFileModified = 1 << 0, // Any CSS or JavaScript file was changed in the current variety _TPCThemeMonitoringResultCriticalFileDeleted = 1 << 1, // The design.css or scripts.js file of any variety was deleted _TPCThemeMonitoringResultVarietyCreated = 1 << 2, // A variety was created _TPCThemeMonitoringResultVarietyDeleted = 1 << 3, // A variety was deleted _TPCThemeMonitoringResultThemeDeleted = 1 << 4 // The theme was deleted }; @class TPCThemeVariety; @interface TPCTheme () @property (nonatomic, copy, readwrite) NSString *name; @property (nonatomic, copy, readwrite) NSURL *originalURL; @property (nonatomic, copy, readwrite) NSURL *temporaryURL; @property (nonatomic, assign, readwrite) TPCThemeStorageLocation storageLocation; @property (nonatomic, assign, readwrite) BOOL usable; @property (nonatomic, strong) TPCThemeVariety *globalVariety; @property (nonatomic, strong, nullable) TPCThemeVariety *variety; @property (nonatomic, copy) NSArray *varieties; @property (nonatomic, strong, nullable) NSCache *templateCache; @property (nonatomic, copy, readwrite) NSArray *cssFiles; @property (nonatomic, copy, readwrite) NSArray *jsFiles; @property (nonatomic, copy, readwrite) NSArray *temporaryCSSFiles; @property (nonatomic, copy, readwrite) NSArray *temporaryJSFiles; @property (nonatomic, copy, readwrite) NSArray *templateRepositories; @property (nonatomic, strong) GRMustacheTemplateRepository *defaultTemplateRepository; @property (nonatomic, strong, readwrite) TPCThemeSettings *settings; @property (nonatomic, strong, nullable) XRFileSystemMonitor *fileSystemMonitor; @end @interface TPCThemeVariety : NSObject @property (nonatomic, copy) NSURL *url; @property (nonatomic, weak) TPCTheme *theme; @property (nonatomic, assign) BOOL isGlobalVariety; @property (nonatomic, assign) TPCThemeAppearanceType appearance; @property (nonatomic, copy, nullable) NSURL *cssFile; @property (nonatomic, copy, nullable) NSURL *jsFile; @property (nonatomic, copy) NSDictionary *settings; @property (nonatomic, strong, nullable) GRMustacheTemplateRepository *templateRepository; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithURL:(NSURL *)url NS_DESIGNATED_INITIALIZER; - (BOOL)_reevaluateFileDuringMonitoringAtURL:(NSURL *)fileURL; @end @interface TPCThemeSettings () @property (nonatomic, assign, readwrite) BOOL supportsMultipleAppearances; @property (nonatomic, assign, readwrite) BOOL invertSidebarColors; @property (nonatomic, assign, readwrite) BOOL js_postHandleEventNotifications; @property (nonatomic, assign, readwrite) BOOL js_postAppearanceChangesNotification; @property (nonatomic, assign, readwrite) BOOL js_postPreferencesDidChangesNotifications; @property (nonatomic, assign, readwrite) BOOL usesIncompatibleTemplateEngineVersion; @property (nonatomic, copy, readwrite, nullable) NSFont *themeChannelViewFont; @property (nonatomic, copy, readwrite, nullable) NSString *themeNicknameFormat; @property (nonatomic, copy, readwrite, nullable) NSString *themeTimestampFormat; @property (nonatomic, copy, readwrite, nullable) NSString *settingsKeyValueStoreName; @property (nonatomic, copy, readwrite, nullable) NSColor *channelViewOverlayColor; @property (nonatomic, copy, readwrite, nullable) NSColor *underlyingWindowColor; @property (nonatomic, copy, readwrite, nullable) NSURL *cssFile; @property (nonatomic, copy, readwrite, nullable) NSURL *jsFile; @property (nonatomic, assign, readwrite) double indentationOffset; @property (nonatomic, assign, readwrite) TPCThemeSettingsNicknameColorStyle nicknameColorStyle; @property (nonatomic, assign, readwrite) NSUInteger templateEngineVersion; - (instancetype)initWithTheme:(TPCTheme *)theme NS_DESIGNATED_INITIALIZER; @end @implementation TPCTheme #pragma mark - #pragma mark Initialization - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithURL:(NSURL *)url inStorageLocation:(TPCThemeStorageLocation)storageLocation { NSParameterAssert(url != nil); NSParameterAssert(url.isFileURL); NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); if ((self = [super init])) { NSURL *originalURL = url.URLByStandardizingPath; self.name = originalURL.lastPathComponent; self.originalURL = originalURL; self.storageLocation = storageLocation; [self _loadTheme]; return self; } return nil; } - (void)dealloc { [self _stopMonitoring]; } - (void)_loadTheme { [self _assignTemporaryURL]; [self _loadGlobalVariety]; [self _loadVarieties]; if (self.varieties == nil) { self.varieties = @[]; } /* During init there should be not variety already set. */ self.usable = ([self _chooseBestVariety] == _TPCThemeChooseVarietyResultChanged); [self _startMonitoring]; } - (void)_loadGlobalVariety { NSURL *url = self.originalURL; TPCThemeVariety *variety = [[TPCThemeVariety alloc] initWithURL:url]; variety.isGlobalVariety = YES; self.globalVariety = variety; } - (void)_loadVarieties { NSURL *varietiesURL = [self _varietiesURL]; if ([RZFileManager() fileExistsAtURL:varietiesURL] == NO) { return; } NSError *preFileListError; NSArray *preFileList = [RZFileManager() contentsOfDirectoryAtURL:varietiesURL includingPropertiesForKeys:@[NSURLNameKey, NSURLIsDirectoryKey] options:NSDirectoryEnumerationSkipsHiddenFiles error:&preFileListError]; if (preFileListError) { LogToConsoleError("Failed to list contents of Varieties folder: %{public}@", preFileListError.localizedDescription); } NSMutableArray *varieties = [NSMutableArray array]; for (NSURL *fileURL in preFileList) { NSNumber *isDirectory = [fileURL resourceValueForKey:NSURLIsDirectoryKey]; if ([isDirectory boolValue] == NO) { continue; } TPCThemeVariety *variety = [[TPCThemeVariety alloc] initWithURL:fileURL]; [varieties addObject:variety]; } self.varieties = varieties; } - (NSURL *)_varietiesURL { NSURL *url = self.originalURL; return [url URLByAppendingPathComponent:@"Varieties/"]; } - (void)_populateSettings { self.settings = [[TPCThemeSettings alloc] initWithTheme:self]; } - (void)_assignDefaultTemplateRepository { NSURL *repositoryURL = [self _applicationTemplateRepositoryURL]; GRMustacheTemplateRepository *repository = [GRMustacheTemplateRepository templateRepositoryWithBaseURL:repositoryURL]; NSAssert((repository != nil), @"Default template repository not found"); self.defaultTemplateRepository = repository; } - (void)_assignTemporaryURL { // NSURL *sourceURL = [TPCPathInfo applicationTemporaryProcessSpecificURL]; NSURL *sourceURL = [TPCPathInfo applicationCachesURL]; NSURL *baseURL = [sourceURL URLByAppendingPathComponent:@"/Cached-Style-Resources/"]; self.temporaryURL = baseURL.URLByStandardizingPath; } #pragma mark - #pragma mark Monitoring - (BOOL)_isDirectoryURLSelf:(NSURL *)url { NSURL *selfURL = self.originalURL; return [self _isDirectoryURL:url equalTo:selfURL]; } - (BOOL)_isDirectoryURL:(NSURL *)url1 equalTo:(NSURL *)url2 { NSParameterAssert(url1 != nil); NSParameterAssert(url2 != nil); /* The file representation is compared instead of the resource identifier because the resource identifier returns nil when the URL no longer exists. */ return [url1 isEqualByFileRepresentation:url2]; } - (nullable TPCThemeVariety *)_varietyAtURL:(NSURL *)url { NSParameterAssert(url != nil); TPCThemeVariety *globalVariety = self.globalVariety; NSURL *globalVarietyURL = globalVariety.url; if ([self _isDirectoryURL:url equalTo:globalVarietyURL]) { return globalVariety; } NSArray *varieties = self.varieties; TPCThemeVariety *variety = [varieties objectPassingTest:^BOOL(TPCThemeVariety *variety, NSUInteger index, BOOL *stop) { NSURL *varietyURL = variety.url; return [self _isDirectoryURL:url equalTo:varietyURL]; }]; return variety; } - (void)_stopMonitoring { XRFileSystemMonitor *monitor = self.fileSystemMonitor; if (monitor == nil) { return; } [monitor stopMonitoring]; self.fileSystemMonitor = nil; } - (void)_startMonitoring { NSURL *url = self.originalURL; __weak TPCTheme *weakSelf = self; XRFileSystemMonitor *monitor = [[XRFileSystemMonitor alloc] initWithFileURL:url callbackBlock:^(NSArray *events) { [weakSelf _reactToMonitoringEvents:events]; }]; [monitor startMonitoringWithLatency:5.0]; self.fileSystemMonitor = monitor; } - (void)_concludeMonitoringEventWithResult:(_TPCThemeMonitoringResult)result { /* Theme was deleted */ if ((result & _TPCThemeMonitoringResultThemeDeleted) == _TPCThemeMonitoringResultThemeDeleted) { [self _reactToDeletion]; return; } /* Theme was modified in such a way that it must be validated and possibly a new best choice is chosen. */ if ((result & _TPCThemeMonitoringResultCriticalFileDeleted) == _TPCThemeMonitoringResultCriticalFileDeleted || (result & _TPCThemeMonitoringResultVarietyCreated) == _TPCThemeMonitoringResultVarietyCreated || (result & _TPCThemeMonitoringResultVarietyDeleted) == _TPCThemeMonitoringResultVarietyDeleted) { [self _verifyIntegrity]; } /* CSS or JavaScript file in the current variety was modified. */ else if ((result & _TPCThemeMonitoringResultReloadableFileModified) == _TPCThemeMonitoringResultReloadableFileModified) { [self _notifyRecentlyModified]; } } - (void)_reactToMonitoringEvents:(NSArray *)events { NSParameterAssert(events != nil); _TPCThemeMonitoringResult result = _TPCThemeMonitoringResultNoChange; for (XRFileSystemEvent *event in events) { result |= [self _reactToMonitoringEventAtURL:event.url withFlags:event.flags]; } [self _concludeMonitoringEventWithResult:result]; } - (_TPCThemeMonitoringResult)_reactToMonitoringEventAtURL:(NSURL *)url withFlags:(FSEventStreamEventFlags)flags { NSParameterAssert(url != nil); /* Returns YES if something changed that requires an integrity check. NO otherwise. */ if (flags & kFSEventStreamEventFlagItemIsFile) { return [self _reactToMonitoringEventForFileAtURL:url withFlags:flags]; } else if (flags & kFSEventStreamEventFlagItemIsDir) { return [self _reactToMonitoringEventForDirectoryAtURL:url withFlags:flags]; } return _TPCThemeMonitoringResultNoChange; // No change } - (_TPCThemeMonitoringResult)_reactToMonitoringEventForFileAtURL:(NSURL *)url withFlags:(FSEventStreamEventFlags)flags { NSParameterAssert(url != nil); NSURL *directoryURL = url.URLByDeletingLastPathComponent; TPCThemeVariety *variety = [self _varietyAtURL:directoryURL]; /* The monitor is used for two parts: 1. To continuously verify the integrity of the theme and varieties so that the next time that it's reloaded, it will be in a usable state. 2. To automatically reload the theme when CSS and JavaScript files changed if that's what the user has configured. If #1 is triggered, then we do not do #2. */ if (variety == nil) { return _TPCThemeMonitoringResultNoChange; } _TPCThemeMonitoringResult result = _TPCThemeMonitoringResultNoChange; BOOL varietyChanged = [self _verifyIntegrityOfFileAtURL:url duringMonitoringOfVariety:variety]; if (varietyChanged) { result |= _TPCThemeMonitoringResultCriticalFileDeleted; } /* Limit #2 to scope of active variety. */ if (variety != self.variety && variety != self.globalVariety) { return result; } /* Do #2 */ NSString *fileExtension = url.pathExtension; if ([fileExtension isEqual:@"css"] || [fileExtension isEqual:@"js"]) { result |= _TPCThemeMonitoringResultReloadableFileModified; } return result; // No change } - (_TPCThemeMonitoringResult)_reactToMonitoringEventForDirectoryAtURL:(NSURL *)url withFlags:(FSEventStreamEventFlags)flags { NSParameterAssert(url != nil); /* React to changes to the theme itself. */ if ([self _isDirectoryURLSelf:url]) { if ([RZFileManager() directoryExistsAtURL:url] == NO) { return _TPCThemeMonitoringResultThemeDeleted; } return _TPCThemeMonitoringResultNoChange; } /* React to changes to a specific variety folder. We determine which URLs to target by comparing the parent of this URL to the Varieties directory URL. */ NSURL *parentURL = url.URLByDeletingLastPathComponent; NSURL *varietiesURL = [self _varietiesURL]; if ([self _isDirectoryURL:parentURL equalTo:varietiesURL]) { return [self _reactToMonitoringVarietyDirectoryEventAtURL:url withFlags:flags]; } return _TPCThemeMonitoringResultNoChange; } - (_TPCThemeMonitoringResult)_reactToMonitoringVarietyDirectoryEventAtURL:(NSURL *)url withFlags:(FSEventStreamEventFlags)flags { NSParameterAssert(url != nil); BOOL varietyDeleted = ([RZFileManager() directoryExistsAtURL:url] == NO); TPCThemeVariety *variety = [self _varietyAtURL:url]; NSMutableArray *varieties = self.varieties.mutableCopy; if (variety) { [varieties removeObject:variety]; } else { if (varietyDeleted) { return _TPCThemeMonitoringResultNoChange; // No change } } if (varietyDeleted == NO) { TPCThemeVariety *newVariety = [[TPCThemeVariety alloc] initWithURL:url]; [varieties addObject:newVariety]; } self.varieties = varieties; return ((varietyDeleted) ? _TPCThemeMonitoringResultVarietyDeleted : _TPCThemeMonitoringResultVarietyCreated); } - (void)_reactToDeletion { [self _stopMonitoring]; [self _changeVariety:nil]; self.usable = NO; [self _notifyDeleted]; } - (void)_notifyRecentlyModified { [RZNotificationCenter() postNotificationName:TPCThemeWasModifiedNotification object:self]; } - (void)_notifyDeleted { [RZNotificationCenter() postNotificationName:TPCThemeWasDeletedNotification object:self]; } #pragma mark - #pragma mark Integrity - (BOOL)_verifyIntegrityOfFileAtURL:(NSURL *)url duringMonitoringOfVariety:(TPCThemeVariety *)variety { NSParameterAssert(url != nil); NSParameterAssert(variety != nil); /* Returns YES if a change is made to property. NO otherwise. */ /* The variety will first determine which type of file was changed. CSS or JavaScript. • If the property for this file is set and the file no longer exists, then the property is set to nil. • If the property for this file is nil and the file exists, then the property is set to the URL of the file. After action is performed by the variety, we can decide wether to do anything depending on whether a change actually took place. */ BOOL fileChanged = [variety _reevaluateFileDuringMonitoringAtURL:url]; if (fileChanged == NO) { return NO; // No change } return YES; // Change made } - (BOOL)_verifyIntegrity { /* The variety changed in some way as described above. The theme will now try to choose the best variety again. If there is not a suitable variety to change to, then at this point integrity of the theme is considered compromised. It is possible to recover from the compromised state by changing this variety or the global variety in such a way that either can be used. */ _TPCThemeChooseVarietyResult varietyChanged = [self _chooseBestVariety]; if (self.usable) { if (varietyChanged == _TPCThemeChooseVarietyResultNoChange) { return NO; // No change } if (varietyChanged == _TPCThemeChooseVarietyResultNoBestChoice) { self.usable = NO; [self _chooseNoVariety]; // Reset selection [RZNotificationCenter() postNotificationName:TPCThemeIntegrityCompromisedNotification object:self]; } } else // usable { if (varietyChanged == _TPCThemeChooseVarietyResultNoBestChoice) { return NO; // No change } self.usable = YES; [RZNotificationCenter() postNotificationName:TPCThemeIntegrityRestoredNotification object:self]; } // usable return YES; // Change made } #pragma mark - #pragma mark Changing Variety - (void)_combineFiles { TPCThemeVariety *variety = self.variety; if (variety == nil) { self.cssFiles = @[]; self.jsFiles = @[]; self.temporaryCSSFiles = @[]; self.temporaryJSFiles = @[]; self.templateRepositories = @[]; return; } TPCThemeVariety *globalVariety = self.globalVariety; NSMutableArray *cssFiles = [NSMutableArray array]; NSMutableArray *jsFiles = [NSMutableArray array]; NSMutableArray *temporaryCSSFiles = [NSMutableArray array]; NSMutableArray *temporaryJSFiles = [NSMutableArray array]; NSMutableArray *templates = [NSMutableArray array]; NSString *originalRemapPath = self.originalURL.path; if ([originalRemapPath hasSuffix:@"/"]) { originalRemapPath = [originalRemapPath substringAtIndex:0 toLength:(-1)]; } NSString *temporaryRemapPath = self.temporaryURL.path; if ([temporaryRemapPath hasSuffix:@"/"]) { temporaryRemapPath = [temporaryRemapPath substringAtIndex:0 toLength:(-1)]; } NSURL *(^_remapTemporaryFile)(NSURL *) = ^NSURL *(NSURL *url) { NSString *path = url.path; if ([path hasPrefix:originalRemapPath] == NO) { return url; } path = [path substringFromIndex:originalRemapPath.length]; path = [temporaryRemapPath stringByAppendingString:path]; return [NSURL fileURLWithPath:path].URLByStandardizingPath; }; void (^_addCSSFile)(TPCThemeVariety *) = ^(TPCThemeVariety *variety) { NSURL *cssFile = variety.cssFile; if (cssFile == nil) { return; } [cssFiles addObject:cssFile]; [temporaryCSSFiles addObject:_remapTemporaryFile(cssFile)]; }; void (^_addJSFile)(TPCThemeVariety *) = ^(TPCThemeVariety *variety) { NSURL *jsFile = variety.jsFile; if (jsFile == nil) { return; } [jsFiles addObject:jsFile]; [temporaryJSFiles addObject:_remapTemporaryFile(jsFile)]; }; void (^_addTemplates)(TPCThemeVariety *) = ^(TPCThemeVariety *variety) { GRMustacheTemplateRepository *repository = variety.templateRepository; if (repository == nil) { return; } [templates addObject:repository]; }; _addCSSFile(globalVariety); _addJSFile(globalVariety); if (variety.isGlobalVariety == NO) { _addCSSFile(variety); _addJSFile(variety); _addTemplates(variety); } _addTemplates(globalVariety); self.cssFiles = cssFiles; self.jsFiles = jsFiles; self.temporaryCSSFiles = temporaryCSSFiles; self.temporaryJSFiles = temporaryJSFiles; self.templateRepositories = templates; } - (_TPCThemeChooseVarietyResult)_chooseBestVariety { TPCThemeVariety *bestVariety = [self _bestVariety]; if (bestVariety == nil) { return _TPCThemeChooseVarietyResultNoBestChoice; } TPCThemeVariety *currentVariety = self.variety; if (currentVariety == bestVariety) { return _TPCThemeChooseVarietyResultNoChange; } [self _changeVariety:bestVariety]; return _TPCThemeChooseVarietyResultChanged; } - (void)_changeVariety:(nullable TPCThemeVariety *)variety { TPCThemeVariety *previousVariety = self.variety; self.templateCache = nil; self.variety = variety; [self _combineFiles]; [self _populateSettings]; /* Assign the default repository after populating settings as we need the template engine version for construction. */ [self _assignDefaultTemplateRepository]; /* Do not fire notification if there is not a previous variety (during init) or we are in a compromised state. */ if (previousVariety != nil && variety != nil && self.usable) { if (previousVariety.appearance == variety.appearance) { [RZNotificationCenter() postNotificationName:TPCThemeVarietyChangedNotification object:self]; } else { [RZNotificationCenter() postNotificationName:TPCThemeAppearanceChangedNotification object:self]; } } } - (void)_chooseNoVariety { [self _changeVariety:nil]; } - (nullable TPCThemeVariety *)_bestVariety { TXAppearance *appAppearance = [TXSharedApplication sharedAppearance]; BOOL isDarkAppearance = appAppearance.properties.isDarkAppearance; TPCThemeVariety *globalVariety = self.globalVariety; BOOL globalHasCSS = (globalVariety.cssFile != nil); BOOL globalHasJS = (globalVariety.jsFile != nil); TPCThemeVariety *bestVariety = nil; NSArray *varieties = self.varieties; /* A variety does not need to contain a CSS or JavaScript file to be the best. As long those exist within the global variety, then any variety that meets the appearance criteria can be the best. This allows for flexibility such as specific variety containing different templates and/or settings while using a unified CSS and/or JavaScript file. */ for (TPCThemeVariety *variety in varieties) { BOOL isBestVariety = NO; /* Perform first pass based on appearance criteria. */ if ((variety.appearance == TPCThemeAppearanceTypeLight && isDarkAppearance == NO) || (variety.appearance == TPCThemeAppearanceTypeDark && isDarkAppearance)) { isBestVariety = YES; } /* We always set a best appearance even when appearance doesn't match so that we can at least have one to work off of. Of course if we find a better variety while enumerating, such as one that does match the appearance, then the default is discarded. */ else if (bestVariety == nil) { isBestVariety = YES; } if (isBestVariety) { /* Ensure someone has a CSS and JavaScript file. */ if ((globalHasCSS == NO && variety.cssFile == nil) || (globalHasJS == NO && variety.jsFile == nil)) { isBestVariety = NO; } } /* Set as best variety if all conditions are met. */ if (isBestVariety) { bestVariety = variety; } } /* If we do not have a best variety, then use the global variety assuming it can be used. */ if (bestVariety == nil && globalHasCSS && globalHasCSS) { bestVariety = globalVariety; } return bestVariety; } - (void)updateAppearance { [self _chooseBestVariety]; } #pragma mark - #pragma mark Getters - (TPCThemeAppearanceType)appearance { return self.variety.appearance; } - (NSArray *)cssFilePaths { return [self _pathsArrayForURLs:self.cssFiles]; } - (NSArray *)jsFilePaths { return [self _pathsArrayForURLs:self.jsFiles]; } - (NSArray *)temporaryCSSFilePaths { return [self _pathsArrayForURLs:self.temporaryCSSFiles]; } - (NSArray *)temporaryJSFilePaths { return [self _pathsArrayForURLs:self.temporaryJSFiles]; } - (NSArray *)_pathsArrayForURLs:(NSArray *)urls { NSParameterAssert(urls != nil); return [NSArray pathsArrayForFileURLs:urls]; } #pragma mark - #pragma mark Templates + (NSDictionary *)_templateLineTypes { return [TPCResourceManager dictionaryFromResources:@"TemplateLineTypes"]; } - (NSURL *)_applicationTemplateRepositoryURL { TPCThemeSettings *settings = self.settings; NSString *filename = [NSString stringWithFormat:@"/Style Default Templates/Version %lu/", settings.templateEngineVersion]; NSURL *templatesPath = [[TPCPathInfo applicationResourcesURL] URLByAppendingPathComponent:filename]; return templatesPath; } - (NSString *)applicationTemplateRepositoryPath { NSURL *repositoryURL = [self _applicationTemplateRepositoryURL]; return repositoryURL.path; } - (nullable GRMustacheTemplate *)templateWithLineType:(TVCLogLineType)type { NSString *typeString = [TVCLogLine stringForLineType:type]; NSString *templateName = [@"Line Types/" stringByAppendingString:typeString]; GRMustacheTemplate *template = [self _templateWithName:templateName logErrors:NO]; if (template == nil) { templateName = [self.class _templateLineTypes][typeString]; if (templateName == nil) { return nil; } template = [self _templateWithName:templateName logErrors:YES]; } return template; } - (nullable GRMustacheTemplate *)templateWithName:(NSString *)templateName { return [self _templateWithName:templateName logErrors:YES]; } - (nullable GRMustacheTemplate *)_templateWithName:(NSString *)templateName logErrors:(BOOL)logErrors { NSParameterAssert(templateName != nil); NSCache *cache = self.templateCache; if (cache == nil) { cache = [NSCache new]; self.templateCache = cache; } else { GRMustacheTemplate *template = [cache objectForKey:templateName]; if (template) { return template; } } GRMustacheTemplate * _Nullable (^_loadTemplate)(GRMustacheTemplateRepository *) = ^GRMustacheTemplate * _Nullable (GRMustacheTemplateRepository *repository) { NSError *loadError = nil; GRMustacheTemplate *template = [repository templateNamed:templateName error:&loadError]; if (loadError && (loadError.code == GRMustacheErrorCodeTemplateNotFound || loadError.code == 260)) { return nil; } if (loadError && logErrors) { LogToConsoleError("Failed to load template '%{public}@' with error: '%{public}@'", templateName, loadError.localizedDescription); LogStackTrace(); } return template; }; GRMustacheTemplate *template = nil; NSArray *repositories = self.templateRepositories; for (GRMustacheTemplateRepository *repository in repositories) { template = _loadTemplate(repository); if (template != nil) { break; } } if (template == nil) { GRMustacheTemplateRepository *repository = self.defaultTemplateRepository; template = _loadTemplate(repository); } if (template != nil) { [cache setObject:template forKey:templateName]; } return template; } @end #pragma mark - #pragma mark Theme Variety @implementation TPCThemeVariety - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithURL:(NSURL *)url { NSParameterAssert(url != nil); if ((self = [super init])) { self.url = url.URLByStandardizingPath; [self _loadVariety]; return self; } return nil; } - (void)_loadVariety { NSURL *url = self.url; /* CSS file */ NSURL *cssFile = [url URLByAppendingPathComponent:@"design.css"]; if ([RZFileManager() fileExistsAtURL:cssFile]) { self.cssFile = cssFile; } /* JavaScript file */ NSURL *jsFile = [url URLByAppendingPathComponent:@"scripts.js"]; if ([RZFileManager() fileExistsAtURL:jsFile]) { self.jsFile = jsFile; } NSURL *templatesURL = [self.class _compatTemplatesAtURL:url]; self.templateRepository = [GRMustacheTemplateRepository templateRepositoryWithBaseURL:templatesURL]; /* Load settings dictionary */ NSURL *settingsURL = [self.class _compatSettingsAtURL:url]; NSDictionary *settings = [NSDictionary dictionaryWithContentsOfURL:settingsURL]; if (settings == nil) { settings = @{}; } self.settings = settings; /* Appearance */ TPCThemeAppearanceType appearance = TPCThemeAppearanceTypeDefault; NSString *appearanceObject = [settings stringForKey:@"Appearance"]; if ([appearanceObject isEqual:@"dark"]) { appearance = TPCThemeAppearanceTypeDark; } else if ([appearanceObject isEqual:@"light"]) { appearance = TPCThemeAppearanceTypeLight; } self.appearance = appearance; } static inline BOOL _reevaluateFileDuringSetOrUnset(NSURL *fileURL, NSURL * __strong *setter) { BOOL fileExists = [RZFileManager() fileExistsAtURL:fileURL]; if (fileExists) { if (*setter == nil) { *setter = [fileURL copy]; return YES; } } else { if (*setter) { *setter = nil; return YES; } } return NO; } - (BOOL)_reevaluateFileDuringMonitoringAtURL:(NSURL *)fileURL { NSParameterAssert(fileURL != nil); NSParameterAssert(fileURL.isFileURL); /* Returns YES if a change is made to property. NO otherwise. */ /* This method is only called for the root of the variety which means we only need to perform file name comparison. */ NSString *filename = fileURL.lastPathComponent; if ([filename isEqual:@"design.css"]) { return _reevaluateFileDuringSetOrUnset(fileURL, &self->_cssFile); } else if ([filename isEqual:@"scripts.js"]) { return _reevaluateFileDuringSetOrUnset(fileURL, &self->_jsFile); } return NO; } #pragma mark - #pragma mark Backwards Compatibility + (NSURL *)_compatSettingsAtURL:(NSURL *)url { NSParameterAssert(url != nil); NSURL *oldURL = [url URLByAppendingPathComponent:@"Data/Settings/styleSettings.plist"]; if ([RZFileManager() fileExistsAtURL:oldURL]) { return oldURL; } return [url URLByAppendingPathComponent:@"settings.plist"]; } + (NSURL *)_compatTemplatesAtURL:(NSURL *)url { NSParameterAssert(url != nil); NSURL *oldURL = [url URLByAppendingPathComponent:@"Data/Templates/"]; if ([RZFileManager() fileExistsAtURL:oldURL]) { return oldURL; } return [url URLByAppendingPathComponent:@"Templates/"]; } @end #pragma mark - #pragma mark Theme Settings @implementation TPCThemeSettings - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithTheme:(TPCTheme *)theme { NSParameterAssert(theme != nil); if ((self = [super init])) { [self _loadSettingsForTheme:theme]; return self; } return nil; } - (void)_loadSettingsForTheme:(TPCTheme *)theme { /* Combine both setting dictionaries */ TPCThemeVariety *globalVariety = theme.globalVariety; NSDictionary *settings = globalVariety.settings; TPCThemeVariety *variety = theme.variety; if (variety && variety.isGlobalVariety == NO) { NSDictionary *settingsNew = variety.settings; if (settings == nil) { settings = settingsNew; } else { settings = [settings dictionaryByAddingEntries:variety.settings]; } } /* Populate settings */ self.themeChannelViewFont = [self.class _fontForKey:@"Override Channel Font" fromDictionary:settings]; self.themeNicknameFormat = [self.class _stringForKey:@"Nickname Format" fromDictionary:settings]; self.themeTimestampFormat = [self.class _stringForKey:@"Timestamp Format" fromDictionary:settings]; self.invertSidebarColors = [settings boolForKey:@"Force Invert Sidebars"]; self.channelViewOverlayColor = [self.class _colorForKey:@"Channel View Overlay Color" fromDictionary:settings]; self.underlyingWindowColor = [self.class _colorForKey:@"Underlying Window Color" fromDictionary:settings]; self.settingsKeyValueStoreName = [self.class _stringForKey:@"Key-value Store Name" fromDictionary:settings]; self.js_postHandleEventNotifications = [settings boolForKey:@"Post Textual.handleEvent() Notifications"]; self.js_postAppearanceChangesNotification = [settings boolForKey:@"Post Textual.appearanceDidChange() Notifications"]; self.js_postPreferencesDidChangesNotifications = [settings boolForKey:@"Post Textual.preferencesDidChange() Notifications"]; /* Disable indentation? */ id indentationOffset = settings[@"Indentation Offset"]; if (indentationOffset == nil) { self.indentationOffset = TPCThemeSettingsDisabledIndentationOffset; } else { double indentationOffsetDouble = [indentationOffset doubleValue]; if (indentationOffsetDouble < 0.0) { self.indentationOffset = TPCThemeSettingsDisabledIndentationOffset; } else { self.indentationOffset = indentationOffsetDouble; } } /* Nickname color style */ TPCThemeAppearanceType appearance = variety.appearance; id nicknameColorStyle = settings[@"Nickname Color Style"]; if ([nicknameColorStyle isEqual:@"HSL-light"]) { self.nicknameColorStyle = TPCThemeSettingsNicknameColorStyleLight; } else if ([nicknameColorStyle isEqual:@"HSL-dark"]) { self.nicknameColorStyle = TPCThemeSettingsNicknameColorStyleDark; } else if (appearance == TPCThemeAppearanceTypeLight) { self.nicknameColorStyle = TPCThemeSettingsNicknameColorStyleLight; } else if (appearance == TPCThemeAppearanceTypeDark) { self.nicknameColorStyle = TPCThemeSettingsNicknameColorStyleDark; } else { if (self.underlyingWindowColorIsDark == NO) { self.nicknameColorStyle = TPCThemeSettingsNicknameColorStyleLight; } else { self.nicknameColorStyle = TPCThemeSettingsNicknameColorStyleDark; } } /* Get style template version */ BOOL usesIncompatibleTemplateEngineVersion = YES; NSUInteger templateEngineVersion = 0; NSDictionary *templateVersions = [settings dictionaryForKey:@"Template Engine Versions"]; { NSString *applicationVersion = [TPCApplicationInfo applicationVersionShort]; NSUInteger targetVersion = [templateVersions unsignedIntegerForKey:applicationVersion]; if (NSNumberInRange(targetVersion, _templateEngineVersionMinimum, _templateEngineVersionMaximum)) { templateEngineVersion = targetVersion; usesIncompatibleTemplateEngineVersion = NO; } } if (templateEngineVersion == 0) { NSUInteger defaultVersion = [templateVersions unsignedIntegerForKey:@"default"]; if (NSNumberInRange(defaultVersion, _templateEngineVersionMinimum, _templateEngineVersionMaximum)) { templateEngineVersion = defaultVersion; usesIncompatibleTemplateEngineVersion = NO; } } if (templateEngineVersion == 0) { templateEngineVersion = _templateEngineVersionMaximum; } self.usesIncompatibleTemplateEngineVersion = usesIncompatibleTemplateEngineVersion; self.templateEngineVersion = templateEngineVersion; } #pragma mark - #pragma mark Getters - (BOOL)underlyingWindowColorIsDark { NSColor *windowColor = self.underlyingWindowColor; if (windowColor == nil) { return NO; } @try { CGFloat brightness = windowColor.brightnessComponent; if (brightness < 0.5) { return YES; } } @catch (NSException *exception) { LogToConsoleError("Caught exception: %{public}@", exception.reason); LogStackTrace(); } return NO; } #pragma mark - #pragma mark Setting Loaders + (nullable NSString *)_stringForKey:(NSString *)key fromDictionary:(NSDictionary *)dic { NSParameterAssert(key != nil); NSParameterAssert(dic != nil); NSString *stringValue = [dic stringForKey:key]; /* An empty string should not be considered a valid value */ if (stringValue.length == 0) { return nil; } return stringValue; } + (nullable NSColor *)_colorForKey:(NSString *)key fromDictionary:(NSDictionary *)dic { NSParameterAssert(key != nil); NSParameterAssert(dic != nil); NSString *colorValue = [dic stringForKey:key]; return [NSColor colorWithHexadecimalValue:colorValue]; } + (nullable NSFont *)_fontForKey:(NSString *)key fromDictionary:(NSDictionary *)dic { NSParameterAssert(key != nil); NSParameterAssert(dic != nil); NSDictionary *fontDictionary = [dic dictionaryForKey:key]; if (fontDictionary == nil) { return nil; } NSString *fontName = [fontDictionary stringForKey:@"Font Name"]; if (fontName == nil || [NSFont fontIsAvailable:fontName] == NO) { return nil; } CGFloat fontSize = [fontDictionary doubleForKey:@"Font Size"]; if (fontSize < 5.0) { return nil; } return [NSFont fontWithName:fontName size:fontSize]; } #pragma mark - #pragma mark Style Settings - (nullable NSString *)_keyValueStoreName { NSString *storeName = self.settingsKeyValueStoreName; if (storeName.length == 0) { return nil; } return [NSString stringWithFormat:@"Internal Theme Settings Key-value Store -> %@", storeName]; } - (nullable id)styleSettingsRetrieveValueForKey:(NSString *)key error:(NSString * _Nullable *)resultError { if (key == nil || key.length == 0) { if ( resultError) { *resultError = @"Empty key value"; } return nil; } NSString *storeKey = [self _keyValueStoreName]; if (storeKey == nil) { if ( resultError) { *resultError = @"Empty key-value store name in settings.plist — Set the key \"Key-value Store Name\" in settings.plist as a string. The current style name is the recommended value."; } return nil; } NSDictionary *styleSettings = [RZUserDefaults() dictionaryForKey:storeKey]; if (styleSettings == nil) { return nil; } return styleSettings[key]; } - (BOOL)styleSettingsSetValue:(nullable id)objectValue forKey:(NSString *)objectKey error:(NSString * _Nullable *)resultError { if (objectKey == nil || objectKey.length <= 0) { if (resultError) { *resultError = @"Empty key value"; } return NO; } NSString *storeKey = [self _keyValueStoreName]; if (storeKey == nil) { if (resultError) { *resultError = @"Empty key-value store name in settings.plist — Set the key \"Key-value Store Name\" in settings.plist as a string. The current style name is the recommended value."; } return NO; } BOOL removeValue = ( objectValue == nil || [objectValue isKindOfClass:[NSNull class]] || TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN [objectValue isKindOfClass:[WebUndefined class]]); TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END NSDictionary *styleSettings = [RZUserDefaults() dictionaryForKey:storeKey]; NSMutableDictionary *styleSettingsMutable = nil; if (styleSettings == nil) { if (removeValue) { return YES; } styleSettingsMutable = [NSMutableDictionary dictionaryWithCapacity:1]; } else { styleSettingsMutable = [styleSettings mutableCopy]; } if (removeValue) { [styleSettingsMutable removeObjectForKey:objectKey]; } else { styleSettingsMutable[objectKey] = objectValue; } [RZUserDefaults() setObject:[styleSettingsMutable copy] forKey:storeKey]; return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Preferences/Themes/TPCThemeController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXAppearance.h" #import "TXGlobalModels.h" #import "TXMasterController.h" #import "TXMenuController.h" #import "TDCAlert.h" #import "TDCProgressIndicatorSheetPrivate.h" #import "TLOLocalization.h" #import "TPCPathInfo.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesReload.h" #import "TPCResourceManager.h" #import "TPCThemePrivate.h" #import "TPCThemeControllerPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TPCThemeControllerCustomThemeNameBasicPrefix = @"user"; NSString * const TPCThemeControllerCustomThemeNameCompletePrefix = @"user:"; NSString * const TPCThemeControllerBundledThemeNameBasicPrefix = @"resource"; NSString * const TPCThemeControllerBundledThemeNameCompletePrefix = @"resource:"; NSString * const TPCThemeControllerThemeListDidChangeNotification = @"TPCThemeControllerThemeListDidChangeNotification"; typedef NSDictionary *TPCThemeControllerThemeList; typedef NSMutableDictionary *TPCThemeControllerThemeListMutable; /* Copy operation class is responsible for copying the active theme to a different location when a user requests a local copy of the theme. */ @interface TPCThemeControllerCopyOperation : NSObject @property (nonatomic, weak) TPCThemeController *themeController; @property (nonatomic, copy) NSString *themeName; // Name without source prefix @property (nonatomic, copy) NSString *pathBeingCopiedTo; @property (nonatomic, copy) NSString *pathBeingCopiedFrom; @property (nonatomic, assign) TPCThemeStorageLocation destinationLocation; @property (nonatomic, assign) BOOL reloadThemeWhenCopied; // If YES, setThemeName: is called when copy completes. Otherwise, files are copied and nothing happens. @property (nonatomic, assign) BOOL openThemeWhenCopied; @property (nonatomic, strong) TDCProgressIndicatorSheet *progressIndicator; - (void)beginOperation; @end @interface TPCThemeController () @property (nonatomic, copy) NSString *cachedThemeName; @property (nonatomic, copy, readwrite) NSString *cacheToken; @property (nonatomic, strong, readwrite) TPCTheme *theme; @property (nonatomic, strong, nullable) TPCThemeControllerCopyOperation *currentCopyOperation; @property (nonatomic, strong) TPCThemeControllerThemeListMutable bundledThemes; @property (nonatomic, strong) TPCThemeControllerThemeListMutable customThemes; @property (nonatomic, strong) XRFileSystemMonitor *themeMonitor; @end #pragma mark - #pragma mark Theme Controller @implementation TPCThemeController - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.bundledThemes = [NSMutableDictionary dictionary]; self.customThemes = [NSMutableDictionary dictionary]; [self populateThemes]; [self startMonitoringThemes]; [RZNotificationCenter() addObserver:self selector:@selector(applicationAppearanceChanged:) name:TXApplicationAppearanceChangedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(themeIntegrityCompromised:) name:TPCThemeIntegrityCompromisedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(themeWasDeleted:) name:TPCThemeWasDeletedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(themeWasModified:) name:TPCThemeWasModifiedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(themeVarietyChanged:) name:TPCThemeAppearanceChangedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(themeVarietyChanged:) name:TPCThemeVarietyChangedNotification object:nil]; } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Preparing theme controller"); LogToConsoleTerminationProgress("Removing theme controller observers"); [RZNotificationCenter() removeObserver:self]; LogToConsoleTerminationProgress("Removing theme change observers"); [self stopMonitoringThemes]; LogToConsoleTerminationProgress("Empty theme cache"); [self removeTemporaryCopyOfTheme]; } - (NSURL *)originalURL { return self.theme.originalURL; } - (NSString *)originalPath { return self.originalURL.path; } - (NSURL *)temporaryURL { return self.theme.temporaryURL; } - (NSString *)temporaryPath { return self.temporaryURL.path; } - (TPCThemeSettings *)settings { return self.theme.settings; } - (TPCThemeStorageLocation)storageLocation { return self.theme.storageLocation; } - (NSString *)name { return self.theme.name; } - (void)metadata:(void (^ NS_NOESCAPE)(NSString *fileName, TPCThemeStorageLocation storageLocation))metadataBlock ofThemeNamed:(NSString *)themeName { NSParameterAssert(metadataBlock != nil); NSParameterAssert(themeName != nil); NSString *fileName = [self.class extractThemeName:themeName]; if (fileName == nil) { return; } TPCThemeStorageLocation storageLocation = [self.class storageLocationOfThemeWithName:themeName]; if (storageLocation == TPCThemeStorageLocationUnknown) { return; } metadataBlock(fileName, storageLocation); } - (BOOL)themeExists:(NSString *)themeName { TPCTheme *theme = [self themeNamed:themeName createIfNecessary:YES]; return theme.usable; } - (nullable TPCTheme *)themeNamed:(NSString *)themeName { return [self themeNamed:themeName createIfNecessary:NO]; } - (nullable TPCTheme *)themeNamed:(NSString *)themeName createIfNecessary:(BOOL)createIfNecessary { NSParameterAssert(themeName != nil); __block TPCTheme *theme = nil; [self metadata:^(NSString *fileName, TPCThemeStorageLocation storageLocation) { TPCThemeControllerThemeListMutable list = [self mutableListForStorageLocation:storageLocation]; if (list == nil) { return; } NSString *filePath = [self.class pathOfThemeWithFilename:fileName storageLocation:storageLocation]; if (filePath == nil) { return; } NSURL *fileURL = [NSURL fileURLWithPath:filePath isDirectory:YES]; theme = [self themeAtURL:fileURL withFilename:fileName storageLocation:storageLocation inList:list createIfNecessary:createIfNecessary skipFileExists:NO]; } ofThemeNamed:themeName]; return theme; } - (nullable TPCTheme *)themeAtURL:(NSURL *)url withFilename:(NSString *)name storageLocation:(TPCThemeStorageLocation)storageLocation inList:(TPCThemeControllerThemeListMutable)list createIfNecessary:(BOOL)createIfNecessary skipFileExists:(BOOL)skipFileExists { return [self themeAtURL:url withFilename:name storageLocation:storageLocation inList:list createIfNecessary:createIfNecessary wasCreated:NULL skipFileExists:skipFileExists]; } - (nullable TPCTheme *)themeAtURL:(NSURL *)url withFilename:(NSString *)name storageLocation:(TPCThemeStorageLocation)storageLocation inList:(TPCThemeControllerThemeListMutable)list createIfNecessary:(BOOL)createIfNecessary wasCreated:(nullable BOOL *)wasCreated skipFileExists:(BOOL)skipFileExists { NSParameterAssert(url != nil); NSParameterAssert(url.isFileURL); NSParameterAssert(name != nil); NSParameterAssert(name.length > 0); NSParameterAssert(list != nil); TPCTheme *theme = nil; @synchronized (list) { theme = list[name]; } if (theme || (theme == nil && createIfNecessary == NO)) { return theme; } if (skipFileExists == NO && [RZFileManager() directoryExistsAtURL:url] == NO) { return nil; } theme = [[TPCTheme alloc] initWithURL:url inStorageLocation:storageLocation]; [self addTheme:theme withFilename:name storageLocation:storageLocation]; if ( wasCreated) { *wasCreated = YES; } return theme; } - (void)addTheme:(nullable TPCTheme *)theme withFilename:(NSString *)name storageLocation:(TPCThemeStorageLocation)storageLocation { [self add:YES theme:theme withFilename:name storageLocation:storageLocation]; } - (void)removeThemeWithFilename:(NSString *)name storageLocation:(TPCThemeStorageLocation)storageLocation { [self add:NO theme:nil withFilename:name storageLocation:storageLocation]; } - (void)add:(BOOL)addOrRemove theme:(nullable TPCTheme *)theme withFilename:(NSString *)name storageLocation:(TPCThemeStorageLocation)storageLocation { NSParameterAssert((addOrRemove && theme != nil) || addOrRemove == NO); NSParameterAssert(name != nil); NSParameterAssert(name.length > 0); NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); TPCThemeControllerThemeListMutable list = [self mutableListForStorageLocation:storageLocation]; if (list == nil) { return; } @synchronized (list) { if (addOrRemove) { list[name] = theme; } else { [list removeObjectForKey:name]; } } } + (nullable NSString *)pathOfThemeWithName:(NSString *)themeName { return [self pathOfThemeWithName:themeName storageLocation:NULL]; } + (nullable NSString *)pathOfThemeWithName:(NSString *)themeName storageLocation:(nullable TPCThemeStorageLocation *)storageLocationIn { NSParameterAssert(themeName != nil); TPCThemeStorageLocation storageLocation = [self.class storageLocationOfThemeWithName:themeName]; if ( storageLocationIn) { *storageLocationIn = storageLocation; } if (storageLocation == TPCThemeStorageLocationUnknown) { return nil; } NSString *fileName = [self extractThemeName:themeName]; if (fileName == nil) { return nil; } return [self pathOfThemeWithFilename:fileName storageLocation:storageLocation]; } + (nullable NSString *)pathOfThemeWithFilename:(NSString *)name storageLocation:(TPCThemeStorageLocation)storageLocation { NSParameterAssert(name != nil); NSParameterAssert(name.length > 0); NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); NSString *basePath = [self pathOfStorageLocation:storageLocation]; if (basePath == nil) { return nil; } NSString *filePath = [basePath stringByAppendingPathComponent:name]; return filePath.stringByStandardizingPath; } + (nullable NSString *)pathOfStorageLocation:(TPCThemeStorageLocation)storageLocation { NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); switch (storageLocation) { case TPCThemeStorageLocationBundle: { return [TPCPathInfo bundledThemes]; } case TPCThemeStorageLocationCustom: { return [TPCPathInfo customThemes]; } default: { break; } } return nil; } - (nullable TPCThemeControllerThemeListMutable)mutableListForStorageLocation:(TPCThemeStorageLocation)storageLocation { NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); switch (storageLocation) { case TPCThemeStorageLocationBundle: { return self.bundledThemes; } case TPCThemeStorageLocationCustom: { return self.customThemes; } default: { break; } } return nil; } - (void)startMonitoringThemes { NSMutableArray *urls = [NSMutableArray arrayWithCapacity:2]; NSMutableDictionary *context = [NSMutableDictionary dictionaryWithCapacity:2]; void (^_addStorageLocation)(TPCThemeStorageLocation) = ^(TPCThemeStorageLocation storageLocation) { NSString *path = [self.class pathOfStorageLocation:storageLocation]; if (path == nil) { return; } NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES]; [urls addObject:url]; [context setObject:@(storageLocation) forKey:url]; }; _addStorageLocation(TPCThemeStorageLocationCustom); __weak TPCThemeController *weakSelf = self; XRFileSystemMonitor *monitor = [[XRFileSystemMonitor alloc] initWithFileURLs:urls context:context callbackBlock:^(NSArray *events) { [weakSelf reactToMonitoringEvents:events]; }]; [monitor startMonitoringWithLatency:1.0]; self.themeMonitor = monitor; } - (void)stopMonitoringThemes { XRFileSystemMonitor *monitor = self.themeMonitor; if (monitor == nil) { return; } [monitor stopMonitoring]; self.themeMonitor = nil; } - (void)reactToMonitoringEvents:(NSArray *)events { NSParameterAssert(events != nil); for (XRFileSystemEvent *event in events) { [self reactToMonitoringEvent:event]; } } - (void)reactToMonitoringEvent:(XRFileSystemEvent *)event { NSParameterAssert(event != nil); /* The purpose of the theme monitor is to recognize when new themes have appeared so that we can make them an option for the user immediately. To accomplish this we monitor the directory of each storage location. */ /* Monitor is recursive which means we have to use flags and context information to narrow scope of events. */ FSEventStreamEventFlags flags = event.flags; if ((flags & kFSEventStreamEventFlagItemIsDir) != kFSEventStreamEventFlagItemIsDir) { // LogToConsoleDebug("Ignoring monitoring event for non-directory"); return; } /* Each URL that is monitored is assigned the storage location enum value as its context object. We can use this to understand with certainty which storage location an event is related to. */ NSURL *url = event.url; NSURL *parentURL = url.URLByDeletingLastPathComponent; /* If the parent URL of this event does not contain an object, then the event is not related to a subfolder of the storage location. */ NSNumber *parentContext = [self.themeMonitor contextObjectForURL:parentURL]; if (parentContext == nil) { // LogToConsoleDebug("Ignoring monitoring event for unrelated directory"); return; } TPCThemeStorageLocation storageLocation = parentContext.unsignedIntegerValue; [self reactToMonitoringEventAtURL:url storageLocation:storageLocation flags:flags]; } - (void)reactToMonitoringEventAtURL:(NSURL *)url storageLocation:(TPCThemeStorageLocation)storageLocation flags:(FSEventStreamEventFlags)flags { NSParameterAssert(url != nil); NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); /* TPCTheme objects will announce when they are deleted. We are not interested in events related to those. */ if ([RZFileManager() fileExistsAtURL:url] == NO) { return; } /* Create theme */ NSString *themeName = [url resourceValueForKey:NSURLNameKey]; TPCThemeControllerThemeListMutable themeList = [self mutableListForStorageLocation:storageLocation]; BOOL themeCreated = NO; TPCTheme *theme = [self themeAtURL:url withFilename:themeName storageLocation:storageLocation inList:themeList createIfNecessary:YES wasCreated:&themeCreated skipFileExists:YES]; /* Only post notification if the theme was created. */ if (themeCreated == NO) { return; } LogToConsoleDebug("Theme '%{public}@' named '%{public}@' at '%{public}@' created", theme, themeName, url.standardizedTildePath); [RZNotificationCenter() postNotificationName:TPCThemeControllerThemeListDidChangeNotification object:self]; } - (void)populateThemes { [self populateThemesFromStorageLocation:TPCThemeStorageLocationBundle]; [self populateThemesFromStorageLocation:TPCThemeStorageLocationCustom]; } - (void)populateThemesFromStorageLocation:(TPCThemeStorageLocation)storageLocation { NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); NSString *path = [self.class pathOfStorageLocation:storageLocation]; if (path == nil) { return; } TPCThemeControllerThemeListMutable list = [self mutableListForStorageLocation:storageLocation]; if (list == nil) { return; } NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES]; NSError *preFileListError; NSArray *preFileList = [RZFileManager() contentsOfDirectoryAtURL:url includingPropertiesForKeys:@[NSURLNameKey, NSURLIsDirectoryKey] options:(NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants) error:&preFileListError]; if (preFileListError) { LogToConsoleError("Failed to list contents of theme folder: %{public}@", preFileListError.localizedDescription); } for (NSURL *fileURL in preFileList) { NSNumber *isDirectory = [fileURL resourceValueForKey:NSURLIsDirectoryKey]; if ([isDirectory boolValue] == NO) { continue; } NSString *name = [fileURL resourceValueForKey:NSURLNameKey]; (void)[self themeAtURL:fileURL withFilename:name storageLocation:storageLocation inList:list createIfNecessary:YES skipFileExists:YES]; } } - (TPCThemeControllerThemeList)themesInStorageLocation:(TPCThemeStorageLocation)storageLocation { NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); TPCThemeControllerThemeListMutable list = [self mutableListForStorageLocation:storageLocation]; if (list == nil) { return @{}; } return [list copy]; } - (void)enumerateAvailableThemesWithBlock:(void(NS_NOESCAPE ^)(NSString *fileName, TPCThemeStorageLocation storageLocation, BOOL multipleVariants, BOOL *stop))enumerationBlock { NSParameterAssert(enumerationBlock != nil); /* Create a dictionary of the theme name (file name) as the key, and an array of storage locations it appears within as the value. */ NSMutableDictionary *> *themesMappedByName = [NSMutableDictionary dictionary]; void (^_mapByName)(TPCThemeStorageLocation) = ^(TPCThemeStorageLocation storageLocation) { TPCThemeControllerThemeList themes = [self themesInStorageLocation:storageLocation]; [themes enumerateKeysAndObjectsUsingBlock:^(NSString *name, TPCTheme *theme, BOOL *stop) { if (theme.usable == NO) { return; } NSMutableArray *mappedLocations = themesMappedByName[name]; if (mappedLocations == nil) { mappedLocations = [NSMutableArray array]; themesMappedByName[name] = mappedLocations; } [mappedLocations addObject:@(storageLocation)]; }]; }; _mapByName(TPCThemeStorageLocationBundle); _mapByName(TPCThemeStorageLocationCustom); /* Create sorted list of themes */ NSArray *themeNamesSorted = themesMappedByName.sortedDictionaryKeys; /* Perform enumeration */ BOOL stopEnumeration = NO; for (NSString *themeName in themeNamesSorted) { NSArray *themeLocations = themesMappedByName[themeName]; BOOL multipleVariants = (themeLocations.count > 1); for (NSNumber *themeLocation in themeLocations) { enumerationBlock(themeName, themeLocation.unsignedIntegerValue, multipleVariants, &stopEnumeration); if (stopEnumeration) { break; } } // for themeLocation } // for themeName } - (void)applicationAppearanceChanged:(NSNotification *)notification { [self.theme updateAppearance]; } - (void)themeVarietyChanged:(NSNotification *)notification { [self updatePreferences]; } - (void)themeIntegrityCompromised:(NSNotification *)notification { TPCTheme *theme = notification.object; if (self.theme != theme) { return; } if ([self resetPreferencesForActiveTheme] == NO) { // Validate theme return; } LogToConsoleInfo("Reloading theme because it failed validation"); [TPCPreferences performReloadAction:TPCPreferencesReloadActionStyle]; [RZNotificationCenter() postNotificationName:TPCThemeControllerThemeListDidChangeNotification object:self]; [self presentIntegrityCompromisedAlert]; } - (void)themeWasDeleted:(NSNotification *)notification { TPCTheme *theme = notification.object; [self removeThemeWithFilename:theme.name storageLocation:theme.storageLocation]; if (self.theme != theme) { [RZNotificationCenter() postNotificationName:TPCThemeControllerThemeListDidChangeNotification object:self]; return; } if ([self resetPreferencesForActiveTheme] == NO) { // Validate theme LogToConsoleFault("This should be an impossible condition"); LogStackTrace(); return; } LogToConsoleInfo("Reloading theme because it was deleted"); [TPCPreferences performReloadAction:TPCPreferencesReloadActionStyle]; [RZNotificationCenter() postNotificationName:TPCThemeControllerThemeListDidChangeNotification object:self]; } - (void)themeWasModified:(NSNotification *)notification { TPCTheme *theme = notification.object; if (self.theme != theme) { return; } if ([TPCPreferences automaticallyReloadCustomThemesWhenTheyChange] == NO) { return; } LogToConsoleInfo("Reloading theme because it was modified"); [TPCPreferences performReloadAction:TPCPreferencesReloadActionStyle]; } - (void)load { /* resetPreferencesForPreferredTheme is called for the configured theme before the first ever -reload is called to recover from a style being deleted while the app was closed. */ [self resetPreferencesForPreferredTheme]; [self reload]; } - (void)reload { NSString *themeName = [TPCPreferences themeName]; TPCTheme *theme = [self themeNamed:themeName createIfNecessary:YES]; NSAssert1((theme != nil), @"Missing style resource files: %@", themeName); if (self.theme != theme) { self.theme = theme; } else { return; } self.cachedThemeName = themeName; self.cacheToken = [NSString stringWithUnsignedInteger:TXRandomNumber(1000000)]; [self updatePreferences]; [self createTemporaryCopyOfTheme]; [self presentCompatibilityAlert]; [self presentInvertSidebarColorsAlert]; } - (void)updatePreferences { /* Inform our defaults controller about a few overrides. */ /* These setValue calls basically tell the NSUserDefaultsController for the "Preferences" window that the active theme has overrode a few user configurable options. The window then blanks out the options specified to prevent the user from modifying. */ TPCThemeSettings *settings = self.settings; [TPCPreferences setThemeChannelViewFontPreferenceUserConfigurable:(settings.themeChannelViewFont == nil)]; [TPCPreferences setThemeNicknameFormatPreferenceUserConfigurable:(settings.themeNicknameFormat.length == 0)]; [TPCPreferences setThemeTimestampFormatPreferenceUserConfigurable:(settings.themeTimestampFormat.length == 0)]; } - (void)recreateTemporaryCopyOfThemeIfNecessary { NSURL *temporaryURL = self.temporaryURL; if ([RZFileManager() fileExistsAtURL:temporaryURL]) { return; } [self createTemporaryCopyOfTheme]; } - (void)removeTemporaryCopyOfTheme { NSURL *temporaryURL = self.temporaryURL; if ([RZFileManager() fileExistsAtURL:temporaryURL] == NO) { return; } NSError *removeItemError = nil; if ([RZFileManager() removeItemAtURL:temporaryURL error:&removeItemError] == NO) { LogToConsoleError("Failed to remove temporary directory: %{public}@", removeItemError.localizedDescription); } } - (void)createTemporaryCopyOfTheme { NSURL *originalURL = self.originalURL; NSURL *temporaryURL = self.temporaryURL; [RZFileManager() replaceItemAtURL:temporaryURL withItemAtURL:originalURL options:CSFileManagerOptionsRemoveIfExists]; } - (void)presentCompatibilityAlert { if (self.settings.usesIncompatibleTemplateEngineVersion == NO) { return; } NSUInteger themeNameHash = self.cachedThemeName.hash; NSString *suppressionKey = [NSString stringWithFormat: @"incompatible_theme_dialog_%lu", themeNameHash]; [TDCAlert alertWithMessage:TXTLS(@"Prompts[76t-pn]") title:TXTLS(@"Prompts[py0-cr]", self.name) defaultButton:TXTLS(@"Prompts[2a3-5s]") alternateButton:TXTLS(@"Prompts[c7s-dq]") suppressionKey:suppressionKey suppressionText:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { if (buttonClicked != TDCAlertResponseDefault) { return; } [menuController() showStylePreferences:nil]; }]; } - (void)presentInvertSidebarColorsAlert { if (self.settings.invertSidebarColors == NO) { return; } if ([TXSharedApplication sharedAppearance].properties.isDarkAppearance) { return; } NSUInteger themeNameHash = self.cachedThemeName.hash; NSString *suppressionKey = [NSString stringWithFormat: @"theme_appearance_dialog_%lu", themeNameHash]; [TDCAlert alertWithMessage:TXTLS(@"Prompts[193-6o]") title:TXTLS(@"Prompts[ezn-rm]", self.name) defaultButton:TXTLS(@"Prompts[hf0-w3]") alternateButton:TXTLS(@"Prompts[hv0-79]") suppressionKey:suppressionKey suppressionText:nil completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { if (buttonClicked == TDCAlertResponseDefault) { return; } [TPCPreferences setAppearance:TXPreferredAppearanceDark]; [TPCPreferences performReloadAction:TPCPreferencesReloadActionAppearance]; }]; } - (void)presentIntegrityCompromisedAlert { [TDCAlert alertWithMessage:TXTLS(@"Prompts[3wd-gj]") title:TXTLS(@"Prompts[fjw-hj]", self.name) defaultButton:TXTLS(@"Prompts[c4z-2b]") alternateButton:TXTLS(@"Prompts[c7s-dq]") completionBlock:^(TDCAlertResponse buttonClicked, BOOL suppressed, id underlyingAlert) { if (buttonClicked != TDCAlertResponseDefault) { return; } [menuController() showStylePreferences:nil]; }]; } - (void)resetPreferencesForPreferredTheme { NSString *themeName = [TPCPreferences themeName]; [self resetPreferencesForThemeNamed:themeName]; } - (BOOL)resetPreferencesForActiveTheme { NSString *themeName = self.cachedThemeName; return [self resetPreferencesForThemeNamed:themeName]; } - (BOOL)resetPreferencesForThemeNamed:(NSString *)themeName { NSParameterAssert(themeName != nil); NSString *suggestedFontName = nil; NSString *suggestedThemeName = nil; BOOL validationResult = [self performValidationForTheme:themeName suggestedFont:&suggestedFontName suggestedTheme:&suggestedThemeName]; if (validationResult) { return NO; } if (suggestedFontName) { [TPCPreferences setThemeChannelViewFontName:suggestedFontName]; } if (suggestedThemeName) { [TPCPreferences setThemeName:suggestedThemeName]; } return YES; } - (BOOL)performValidationForTheme:(NSString *)validatedTheme suggestedFont:(NSString **)suggestedFontName suggestedTheme:(NSString **)suggestedThemeName { NSParameterAssert(validatedTheme != nil); NSParameterAssert(suggestedFontName != NULL); NSParameterAssert(suggestedThemeName != NULL); /* Validate font */ BOOL keyChanged = NO; NSString *fontName = [TPCPreferences themeChannelViewFontName]; if ([NSFont fontIsAvailable:fontName] == NO) { if ( suggestedFontName) { *suggestedFontName = [TPCPreferences themeChannelViewFontNameDefault]; } keyChanged = YES; } /* Validate theme */ NSString *themeName = [self.class extractThemeName:validatedTheme]; NSString *themeSource = [self.class extractThemeSource:validatedTheme]; LogToConsoleInfo("Performing validation on theme named '%{public}@' with source type of '%{public}@'", themeName, themeSource); /* Note from October 2020 during refactoring: I know this is ugly as hell. Please don't shame me for it. I just don't have the time to improve it. If it works, it works. */ if (themeSource == nil || [themeSource isEqualToString:TPCThemeControllerBundledThemeNameBasicPrefix]) { /* Remap name of bundled themes. */ NSString *bundledTheme = [self remappedThemeName:validatedTheme]; if (bundledTheme) { if ( suggestedThemeName) { *suggestedThemeName = bundledTheme; keyChanged = YES; } } /* If the theme is faulted and is a bundled theme, then we can do nothing except try to recover by using the default one. */ if (bundledTheme == nil) { bundledTheme = validatedTheme; } if ([self themeExists:bundledTheme] == NO) { if ( suggestedThemeName) { *suggestedThemeName = [TPCPreferences themeNameDefault]; keyChanged = YES; } } // preferred theme exists } // theme source bundled else if ([themeSource isEqualToString:TPCThemeControllerCustomThemeNameBasicPrefix]) { if ([self themeExists:validatedTheme] == NO) { NSString *bundledTheme = [self.class buildFilename:themeName forStorageLocation:TPCThemeStorageLocationBundle]; if ([self themeExists:bundledTheme]) { /* Use a bundled theme with the same name if available. */ if ( suggestedThemeName) { *suggestedThemeName = bundledTheme; keyChanged = YES; } } else { /* Revert back to the default theme if no recovery is possible. */ if ( suggestedThemeName) { *suggestedThemeName = [TPCPreferences themeNameDefault]; keyChanged = YES; } } // bundled theme exists } // preferred theme exists } // theme source custom return (keyChanged == NO); } - (nullable NSString *)remappedThemeName:(NSString *)themeName { NSParameterAssert(themeName != nil); NSDictionary *cachedValues = [TPCResourceManager dictionaryFromResources:@"StaticStore" key:@"TPCThemeController Remapped Themes"]; return cachedValues[themeName]; } - (BOOL)isBundledTheme { return (self.storageLocation == TPCThemeStorageLocationBundle); } + (nullable NSString *)buildFilename:(NSString *)name forStorageLocation:(TPCThemeStorageLocation)storageLocation { NSParameterAssert(name != nil); NSParameterAssert(name.length > 0); NSParameterAssert(storageLocation != TPCThemeStorageLocationUnknown); switch (storageLocation) { case TPCThemeStorageLocationBundle: { return [TPCThemeControllerBundledThemeNameCompletePrefix stringByAppendingString:name]; } case TPCThemeStorageLocationCustom: { return [TPCThemeControllerCustomThemeNameCompletePrefix stringByAppendingString:name]; } default: { break; } } return nil; } + (nullable NSString *)descriptionForStorageLocation:(TPCThemeStorageLocation)storageLocation { switch (storageLocation) { case TPCThemeStorageLocationBundle: { return TXTLS(@"BasicLanguage[7lm-bq]"); } case TPCThemeStorageLocationCustom: { return TXTLS(@"BasicLanguage[bm2-4p]"); } default: { break; } } return nil; } + (nullable NSString *)extractThemeSource:(NSString *)source { NSParameterAssert(source != nil); if ([source hasPrefix:TPCThemeControllerCustomThemeNameCompletePrefix] == NO && [source hasPrefix:TPCThemeControllerBundledThemeNameCompletePrefix] == NO) { return nil; } NSInteger colonIndex = [source stringPosition:@":"]; return [source substringToIndex:colonIndex]; } + (nullable NSString *)extractThemeName:(NSString *)source { NSParameterAssert(source != nil); if ([source hasPrefix:TPCThemeControllerCustomThemeNameCompletePrefix] == NO && [source hasPrefix:TPCThemeControllerBundledThemeNameCompletePrefix] == NO) { return nil; } NSInteger colonIndex = [source stringPosition:@":"]; NSString *name = [source substringAfterIndex:colonIndex]; if (name.length == 0) { return nil; } return name; } + (TPCThemeStorageLocation)storageLocationOfThemeWithName:(NSString *)themeName { NSParameterAssert(themeName != nil); if ([themeName hasPrefix:TPCThemeControllerCustomThemeNameCompletePrefix]) { return TPCThemeStorageLocationCustom; } else if ([themeName hasPrefix:TPCThemeControllerBundledThemeNameCompletePrefix]) { return TPCThemeStorageLocationBundle; } return TPCThemeStorageLocationUnknown; } - (void)copyActiveThemeToDestinationLocation:(TPCThemeStorageLocation)destinationLocation reloadOnCopy:(BOOL)reloadOnCopy openOnCopy:(BOOL)openOnCopy { NSAssert((self.currentCopyOperation == nil), @"Tried to create a new copy operation with operation already in progress"); if (self.storageLocation == destinationLocation) { LogToConsoleError("Tried to copy active theme to same storage location that it already exists within"); return; } if (TPCThemeStorageLocationBundle == destinationLocation) { LogToConsoleError("Tried to copy active theme to the application itself"); return; } TPCThemeControllerCopyOperation *copyOperation = [TPCThemeControllerCopyOperation new]; copyOperation.themeController = self; copyOperation.themeName = self.name; copyOperation.pathBeingCopiedFrom = self.originalPath; copyOperation.destinationLocation = destinationLocation; copyOperation.openThemeWhenCopied = openOnCopy; copyOperation.reloadThemeWhenCopied = reloadOnCopy; [copyOperation beginOperation]; self.currentCopyOperation = copyOperation; } - (void)copyActiveThemeOperationCompleted { self.currentCopyOperation = nil; } @end #pragma mark - #pragma mark Theme Controller Copy Operation @implementation TPCThemeControllerCopyOperation - (void)beginOperation { /* Setup progress indicator. */ TDCProgressIndicatorSheet *progressIndicator = [[TDCProgressIndicatorSheet alloc] initWithWindow:[NSApp keyWindow]]; self.progressIndicator = progressIndicator; [self.progressIndicator start]; /* All work is done in a background thread. */ /* Once started, the operation cannot be cancelled. It will occur then it will either call -cancelOperation itself on failure or wait for the theme controller itself to call -completeOperation which signals to the copier that the theme controller sees the files. */ XRPerformBlockAsynchronouslyOnGlobalQueue(^{ [self _beginOperation]; }); } - (void)_beginOperation { [self _defineDestinationPath]; NSURL *sourceURL = [NSURL fileURLWithPath:self.pathBeingCopiedFrom isDirectory:YES]; NSURL *destinationURL = [NSURL fileURLWithPath:self.pathBeingCopiedTo isDirectory:YES]; if ([RZFileManager() replaceItemAtURL:destinationURL withItemAtURL:sourceURL options:(CSFileManagerOptionsMoveToTrash | CSFileManagerOptionsRemoveIfExists)] == NO) { [self cancelOperation]; return; } [self completeOperation]; } - (void)_defineDestinationPath { NSString *destinationPath = nil; destinationPath = [TPCPathInfo customThemes]; destinationPath = [destinationPath stringByAppendingPathComponent:self.themeName]; /* Cast as nonnull to make static analyzer happy */ self.pathBeingCopiedTo = (NSString * _Nonnull)destinationPath; } - (void)cancelOperation { XRPerformBlockAsynchronouslyOnMainQueue(^{ [self _cancelOperation]; }); } - (void)completeOperation { /* The copy process is usually instantaneous so add a slight delay because I like to mess with people */ [NSThread sleepForTimeInterval:3.0]; XRPerformBlockAsynchronouslyOnMainQueue(^{ [self _completeOperation]; }); } - (void)_cancelOperation { [self invalidateOperation]; } - (void)_completeOperation { /* Maybe open new path of theme */ if (self.openThemeWhenCopied) { NSURL *fileURL = [NSURL fileURLWithPath:self.pathBeingCopiedTo]; [RZWorkspace() openURL:fileURL]; } /* Maybe reload new theme */ if (self.reloadThemeWhenCopied) { NSString *newThemeName = [TPCThemeController buildFilename:self.themeName forStorageLocation:self.destinationLocation]; [TPCPreferences setThemeName:newThemeName]; [TPCPreferences performReloadAction:TPCPreferencesReloadActionStyle]; } /* Close progress indicator */ [self invalidateOperation]; } - (void)invalidateOperation { [self.progressIndicator stop]; self.progressIndicator = nil; [self.themeController copyActiveThemeOperationCompleted]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Services/ICLPayloadLocal.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPathInfoPrivate.h" #import "TPCThemeController.h" #import "ICLPayloadInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation ICLPayload (ICLPayloadLocalPrivate) - (NSString *)_resourcesTemporaryLocation { // NSString *sourcePath = [TPCPathInfo applicationTemporaryProcessSpecific]; NSString *sourcePath = themeController().temporaryPath; NSString *basePath = [sourcePath stringByAppendingPathComponent:@"/ICLPayload-Resources/"]; [TPCPathInfo _createDirectoryAtPath:basePath]; return basePath; } /* WebKit2 uses sandboxed processes. We copy the resources files to the application's temporary folder so that it can access them. */ - (nullable NSArray *)_copyResourcesToTemporaryLocation:(nullable NSArray *)resources { if (resources == nil) { return nil; } NSString *basePath = [self _resourcesTemporaryLocation]; NSString *(^copyOperation)(NSURL *) = ^NSString *(NSURL *resourceURL) { if (resourceURL.isFileURL == NO) { return resourceURL.absoluteString; } NSString *resourcePath = resourceURL.relativePath; NSString *filename = [NSString stringWithFormat:@"%@.%@", resourcePath.md5, resourcePath.pathExtension]; NSString *destinationPath = [basePath stringByAppendingPathComponent:filename]; if ([RZFileManager() fileExistsAtPath:destinationPath]) { return destinationPath; } NSError *copyError; BOOL copyResult = [RZFileManager() copyItemAtPath:resourcePath toPath:destinationPath error:©Error]; if (copyResult == NO) { LogToConsoleError("Copy operation for '%{public}@' failed with error: %{public}@", resourcePath.standardizedTildePath, copyError.localizedDescription); } return destinationPath; }; NSMutableArray *temporaryResources = [NSMutableArray arrayWithCapacity:resources.count]; for (NSURL *resourceURL in resources) { @autoreleasepool { [temporaryResources addObject:copyOperation(resourceURL)]; } } return [temporaryResources copy]; } - (NSDictionary *)javaScriptObject { NSMutableDictionary *dic = [NSMutableDictionary dictionary]; [dic setUnsignedInteger:self->_contentLength forKey:@"contentLength"]; [dic setObject:@{ @"width" : @(self->_contentSize.width), @"height" : @(self->_contentSize.height) } forKey:@"contentSize"]; [dic maybeSetObject:[self _copyResourcesToTemporaryLocation:self->_styleResources] forKey:@"styleResources"]; [dic maybeSetObject:[self _copyResourcesToTemporaryLocation:self->_scriptResources] forKey:@"scriptResources"]; [dic setObject:self->_html forKey:@"html"]; NSString *entrypoint = self->_entrypoint; if (entrypoint) { [dic setObject:entrypoint forKey:@"entrypoint"]; /* call self. instead of self->_ for entrypointPayload to allow the default values to be assigned to the exported object. */ [dic setObject:self.entrypointPayload forKey:@"entrypointPayload"]; } [dic setObject:self->_url forKey:@"url"]; [dic setObject:self->_urlToInline forKey:@"urlToInline"]; [dic setObject:self->_lineNumber forKey:@"lineNumber"]; [dic setObject:self->_uniqueIdentifier forKey:@"uniqueIdentifier"]; // [dic setObject:self->_viewIdentifier forKey:@"viewIdentifier"]; [dic setUnsignedInteger:self->_index forKey:@"index"]; return [dic copy]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel Selection Table/TVCChannelSelectionOutlineCellView.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCTreeItem.h" #import "TVCChannelSelectionViewControllerPrivate.h" #import "TVCChannelSelectionOutlineViewCellPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCChannelSelectionOutlineCellView () @property (readonly) IRCTreeItem *cellItem; - (IBAction)selectionCheckboxClicked:(id)sender; @end @implementation TVCChannelSelectionOutlineCellView - (void)prepareInitialState { NSOutlineView *outlineView = self.parentController.outlineView; IRCTreeItem *cellItem = self.cellItem; BOOL isGroupItem = [outlineView isGroupItem:cellItem]; self.textField.stringValue = cellItem.name; self.selectedCheckbox.allowsMixedState = isGroupItem; } - (void)selectionCheckboxClicked:(id)sender { [self.parentController selectionCheckboxClickedInCell:self]; } - (IRCTreeItem *)cellItem { return self.objectValue; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel Selection Table/TVCChannelSelectionViewController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXMasterController.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCWorld.h" #import "TVCChannelSelectionOutlineViewCellPrivate.h" #import "TVCChannelSelectionViewController.h" NS_ASSUME_NONNULL_BEGIN @interface TVCChannelSelectionViewController () @property (nonatomic, strong) IBOutlet NSScrollView *outlineViewScrollView; @property (nonatomic, weak) IBOutlet NSOutlineView *outlineView; @property (nonatomic, weak) NSView *attachedView; @property (nonatomic, strong) NSMutableArray *cachedSelectedClientIds; @property (nonatomic, strong) NSMutableArray *cachedSelectedChannelIds; @property (nonatomic, copy) NSDictionary *> *cachedChannelList; @property (nonatomic, strong) dispatch_source_t expandOutlineViewTimer; @end @implementation TVCChannelSelectionViewController - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TVCChannelSelectionView" owner:self topLevelObjects:nil]; [self addObserverForChannelListUpdates]; [self rebuildCachedChannelList]; } - (void)dealloc { [self removeObserverForChannelListUpdates]; } - (void)attachToView:(NSView *)view { NSParameterAssert(view != nil); if (self.attachedView == nil) { self.attachedView = view; } else { NSAssert(NO, @"Table view is already attached to a view"); } NSScrollView *outlineViewScroller = self.outlineViewScrollView; [view addSubview:outlineViewScroller]; [view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[outlineViewScroller]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(outlineViewScroller)]]; [view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[outlineViewScroller]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(outlineViewScroller)]]; } - (nullable IRCTreeItem *)itemFromCellView:(TVCChannelSelectionOutlineCellView *)cellView { NSParameterAssert(cellView != nil); NSOutlineView *outlineView = self.outlineView; NSInteger cellRow = [outlineView rowForView:cellView]; if (cellRow < 0) { return nil; } return [outlineView itemAtRow:cellRow]; } - (void)selectionCheckboxClickedInCell:(TVCChannelSelectionOutlineCellView *)clickedCell { NSParameterAssert(clickedCell != nil); IRCTreeItem *item = [self itemFromCellView:clickedCell]; NSOutlineView *outlineView = self.outlineView; BOOL isGroupItem = [outlineView isGroupItem:item]; BOOL isEnablingItem = (clickedCell.selectedCheckbox.state == NSControlStateValueOn || clickedCell.selectedCheckbox.state == NSControlStateValueMixed); /* Add or remove item from appropriate filter */ if (isGroupItem) { if (isEnablingItem) { [self.cachedSelectedClientIds addObject:item.uniqueIdentifier]; } else { [self.cachedSelectedClientIds removeObject:item.uniqueIdentifier]; } } else { if (isEnablingItem) { [self.cachedSelectedChannelIds addObject:item.uniqueIdentifier]; } else { [self.cachedSelectedChannelIds removeObject:item.uniqueIdentifier]; } } /* Further process filters depending on state of other items */ if (isGroupItem && isEnablingItem) { NSArray *childrenItems = [outlineView itemsFromParentGroup:item]; for (IRCTreeItem *childItem in childrenItems) { [self.cachedSelectedChannelIds removeObject:childItem.uniqueIdentifier]; } } /* Reload checkbox state for all */ [self updateSelectedStateForItem:item]; /* Call out to delegate */ if ([self.delegate respondsToSelector:@selector(channelSelectionControllerSelectionChanged:)]) { [self.delegate channelSelectionControllerSelectionChanged:self]; } } - (void)updateSelectedStateForItem:(IRCTreeItem *)item { NSOutlineView *outlineView = self.outlineView; IRCTreeItem *parentItem = ((item.isClient) ? item : item.associatedClient); NSInteger parentItemRow = [outlineView rowForItem:parentItem]; TVCChannelSelectionOutlineCellView *parentItemView = [outlineView viewAtColumn:0 row:parentItemRow makeIfNecessary:NO]; BOOL parentItemInFilter = [self.cachedSelectedClientIds containsObject:parentItem.uniqueIdentifier]; BOOL atleastOneChildChecked = NO; NSArray *childrenItems = [outlineView itemsInGroup:parentItem]; for (IRCTreeItem *childItem in childrenItems) { NSInteger childItemRow = [outlineView rowForItem:childItem]; TVCChannelSelectionOutlineCellView *childItemView = [outlineView viewAtColumn:0 row:childItemRow makeIfNecessary:NO]; BOOL childItemInFilter = [self.cachedSelectedChannelIds containsObject:childItem.uniqueIdentifier]; if (parentItemInFilter) { childItemView.selectedCheckbox.state = NSControlStateValueOn; } else if (childItemInFilter) { if (atleastOneChildChecked == NO) { atleastOneChildChecked = YES; } childItemView.selectedCheckbox.state = NSControlStateValueOn; } else { childItemView.selectedCheckbox.state = NSControlStateValueOff; } childItemView.selectedCheckbox.enabled = (parentItemInFilter == NO); } /* Process parent item */ if (parentItemInFilter) { parentItemView.selectedCheckbox.state = NSControlStateValueOn; } else if (atleastOneChildChecked) { parentItemView.selectedCheckbox.state = NSControlStateValueMixed; } else { parentItemView.selectedCheckbox.state = NSControlStateValueOff; } } #pragma mark - #pragma mark Properties - (NSArray *)selectedClientIds { @synchronized (self.cachedSelectedClientIds) { return [self.cachedSelectedClientIds copy]; } } - (NSArray *)selectedChannelIds { @synchronized (self.cachedSelectedChannelIds) { return [self.cachedSelectedChannelIds copy]; } } - (void)setSelectedClientIds:(NSArray *)selectedClientIds { @synchronized (self.cachedSelectedClientIds) { if (self->_cachedSelectedClientIds != selectedClientIds) { self->_cachedSelectedClientIds = [selectedClientIds mutableCopy]; [self reloadOutlineView]; } } } - (void)setSelectedChannelIds:(NSArray *)selectedChannelIds { @synchronized (self.cachedSelectedChannelIds) { if (self->_cachedSelectedChannelIds != selectedChannelIds) { self->_cachedSelectedChannelIds = [selectedChannelIds mutableCopy]; [self reloadOutlineView]; } } } #pragma mark - #pragma mark Cache Management - (void)addObserverForChannelListUpdates { [RZNotificationCenter() addObserver:self selector:@selector(channelListChanged:) name:IRCWorldClientListWasModifiedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(channelListChanged:) name:IRCClientChannelListWasModifiedNotification object:nil]; } - (void)removeObserverForChannelListUpdates { [RZNotificationCenter() removeObserver:self]; } - (void)reloadOutlineView { [self.outlineView reloadData]; } - (void)expandOutlineViewItemsCancelTimer { if (self.expandOutlineViewTimer != nil) { XRCancelScheduledBlock(self.expandOutlineViewTimer); } } - (void)expandOutlineViewItemsCreateTimer { if (self.expandOutlineViewTimer != nil) { return; } __weak TVCChannelSelectionViewController *weakSelf = self; dispatch_source_t expandOutlineViewTimer = XRScheduleBlockOnMainQueue(^{ if (weakSelf == nil) { return; } [weakSelf.outlineView expandItem:nil expandChildren:YES]; weakSelf.expandOutlineViewTimer = nil; }, 0.5); XRResumeScheduledBlock(expandOutlineViewTimer); self.expandOutlineViewTimer = expandOutlineViewTimer; } - (void)channelListChanged:(id)sender { [self rebuildCachedChannelList]; [self reloadOutlineView]; [self expandOutlineViewItemsCancelTimer]; } - (void)rebuildCachedChannelList { NSArray *clientList = worldController().clientList; NSMutableDictionary *> *cachedChannelList = [NSMutableDictionary dictionary]; for (IRCClient *u in clientList) { NSMutableArray *uChannelList = [NSMutableArray array]; for (IRCChannel *c in u.channelList) { if (c.isChannel == NO) { continue; } [uChannelList addObject:c]; } cachedChannelList[(id)u] = [uChannelList copy]; } self.cachedChannelList = cachedChannelList; } #pragma mark - #pragma mark Table View Delegate - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(nullable id)item { if (item) { return self.cachedChannelList[item].count; } return self.cachedChannelList.count; } - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(nullable id)item { if (item) { return self.cachedChannelList[item][index]; } return self.cachedChannelList.allKeys[index]; } - (nullable id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn byItem:(nullable id)item { return item; } - (nullable NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(nullable NSTableColumn *)tableColumn item:(id)item { TVCChannelSelectionOutlineCellView *newView = nil; if ([item isClient]) { newView = (id)[outlineView makeViewWithIdentifier:@"serverEntry" owner:self]; } else { newView = (id)[outlineView makeViewWithIdentifier:@"channelEntry" owner:self]; } newView.parentController = self; return newView; } - (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row { /* Perform work on next pass of the main thread to avoid exception: "insertRowsAtIndexes:withRowAnimation: can not happen while updating visible rows!" */ XRPerformBlockAsynchronouslyOnMainQueue(^{ NSView *cellView = [rowView viewAtColumn:0]; [cellView prepareInitialState]; id item = [outlineView itemAtRow:row]; [self updateSelectedStateForItem:item]; }); [self expandOutlineViewItemsCreateTimer]; } - (BOOL)outlineView:(NSOutlineView *)sender isItemExpandable:(id)item { return YES; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldCollapseItem:(id)item { return NO; } - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item { return NO; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldShowOutlineCellForItem:(id)item { return NO; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/Extras/TVCLogControllerHistoricLogFile.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "HLSHistoricLogProtocol.h" #import "TXMasterController.h" #import "IRCTreeItem.h" #import "IRCWorld.h" #import "TDCAlert.h" #import "TLOLocalization.h" #import "TPCPathInfo.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TVCLogControllerPrivate.h" #import "TVCLogLinePrivate.h" #import "TVCLogLineXPCPrivate.h" #import "TVCLogControllerHistoricLogFilePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCLogControllerHistoricLogFile () @property (nonatomic, assign, readwrite) BOOL isSaving; @property (nonatomic, assign, readwrite) BOOL isTerminating; @property (nonatomic, assign, readwrite) BOOL processLoaded; @property (nonatomic, assign, readwrite) BOOL processLoading; @property (nonatomic, strong) NSXPCConnection *serviceConnection; @property (nonatomic, assign) BOOL connectionInvalidatedVoluntarily; @property (nonatomic, assign) BOOL connectionInvalidatedErrorDialogDisplayed; @property (nonatomic, copy, nullable) NSError *lastServiceConnectionError; @end @implementation TVCLogControllerHistoricLogFile + (TVCLogControllerHistoricLogFile *)sharedInstance { static id sharedSelf = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedSelf = [[self alloc] init]; }); return sharedSelf; } #pragma mark - #pragma mark Save Path - (NSString *)databaseSavePath { return [TPCPathInfo groupContainerApplicationCaches]; } #pragma mark - #pragma mark Construction - (void)warmProcessIfNeeded { if (self.processLoading || self.processLoaded) { return; } LogToConsoleDebug("Warming process..."); self.processLoading = YES; [self connectToService]; [self openDatabase]; [self resetMaximumLineCount]; } - (void)invalidateProcess { if (self.processLoading == NO && self.processLoaded == NO) { return; } LogToConsoleDebug("Invalidating process..."); self.connectionInvalidatedVoluntarily = YES; [self.serviceConnection invalidate]; } - (void)openDatabase { [[self remoteObjectProxyWithErrorHandler:^(NSError *error) { self.processLoading = NO; self.processLoaded = NO; LogToConsoleError("Failed to communicate with process to open database"); }] openDatabaseInDirectory:[self databaseSavePath] withCompletionBlock:^(BOOL success) { if (success) { LogToConsoleDebug("Successfully opened database"); } else { LogToConsoleError("Failed to open database"); } self.processLoading = NO; self.processLoaded = success; }]; } - (void)connectToService { NSXPCConnection *serviceConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.codeux.app-utilities.Textual-ScrollbackHistoryManager"]; NSXPCInterface *remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HLSHistoricLogServerProtocol)]; [remoteObjectInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:ascending:fetchLimit:limitToDate:withCompletionBlock:) argumentIndex:0 ofReply:YES]; [remoteObjectInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:withUniqueIdentifier:beforeFetchLimit:afterFetchLimit:limitToDate:withCompletionBlock:) argumentIndex:0 ofReply:YES]; [remoteObjectInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:beforeUniqueIdentifier:fetchLimit:limitToDate:withCompletionBlock:) argumentIndex:0 ofReply:YES]; [remoteObjectInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:afterUniqueIdentifier:fetchLimit:limitToDate:withCompletionBlock:) argumentIndex:0 ofReply:YES]; [remoteObjectInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:afterUniqueIdentifier:beforeUniqueIdentifier:fetchLimit:withCompletionBlock:) argumentIndex:0 ofReply:YES]; serviceConnection.remoteObjectInterface = remoteObjectInterface; NSXPCInterface *exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HLSHistoricLogClientProtocol)]; serviceConnection.exportedInterface = exportedInterface; serviceConnection.exportedObject = self; serviceConnection.interruptionHandler = ^{ [self interruptionHandler]; LogToConsole("Interruption handler called"); }; serviceConnection.invalidationHandler = ^{ [self invalidationHandler]; LogToConsole("Invalidation handler called"); }; [serviceConnection resume]; self.serviceConnection = serviceConnection; } - (void)interruptionHandler { [self invalidateProcess]; } - (void)invalidationHandler { self.serviceConnection = nil; [self resetContext]; if (self.connectionInvalidatedVoluntarily) { self.connectionInvalidatedVoluntarily = NO; return; } /* Error dialog is purposely only ever shown once */ if (self.connectionInvalidatedErrorDialogDisplayed == NO) { self.connectionInvalidatedErrorDialogDisplayed = YES; } else { return; } NSString *lastErrorMessage = self.lastServiceConnectionError.localizedDescription; if (lastErrorMessage == nil) { lastErrorMessage = @""; } else { lastErrorMessage = TXTLS(@"Prompts[nlz-um]", lastErrorMessage); } [TDCAlert alertWithMessage:lastErrorMessage title:TXTLS(@"Prompts[h99-3q]") defaultButton:TXTLS(@"Prompts[c7s-dq]") alternateButton:nil]; } - (void)resetContext { self.isSaving = NO; self.processLoading = NO; self.processLoaded = NO; } - (void)resetMaximumLineCount { NSUInteger maximumLineCount = [TPCPreferences scrollbackSaveLimit]; [[self remoteObjectProxy] setMaximumLineCount:maximumLineCount]; } - (void)prepareForApplicationTermination { self.isTerminating = YES; [self saveData]; } #pragma mark - #pragma mark Private API - (id )remoteObjectProxy { return [self remoteObjectProxyWithErrorHandler:nil]; } - (id )remoteObjectProxyWithErrorHandler:(void (^ _Nullable)(NSError *error))handler { return [self.serviceConnection remoteObjectProxyWithErrorHandler:^(NSError *error) { self.lastServiceConnectionError = error; LogToConsoleError("Error occurred while communicating with service: %{public}@", error.localizedDescription); if (handler) { handler(error); } }]; } #pragma mark - #pragma mark Public API - (NSArray *)_logLinesFromXPCObjects:(NSArray *)xpcObjects { NSParameterAssert(xpcObjects != nil); NSMutableArray *logLines = [NSMutableArray arrayWithCapacity:xpcObjects.count]; for (TVCLogLineXPC *xpcObject in xpcObjects) { TVCLogLine *logLine = [TVCLogLine logLineFromXPCObject:xpcObject]; if (logLine == nil) { LogToConsoleError("Failed to initialize object %{public}@. Corrupt data?", xpcObject.description); continue; } [logLines addObject:logLine]; } return [logLines copy]; } - (void)fetchEntriesForItem:(IRCTreeItem *)item ascending:(BOOL)ascending fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (^)(NSArray *entries))completionBlock { [self warmProcessIfNeeded]; __weak typeof(self) weakSelf = self; [[self remoteObjectProxy] fetchEntriesForView:item.uniqueIdentifier ascending:ascending fetchLimit:fetchLimit limitToDate:limitToDate withCompletionBlock:^(NSArray *entries) { NSArray *logLines = [weakSelf _logLinesFromXPCObjects:entries]; completionBlock(logLines); }]; } - (void)fetchEntriesForItem:(IRCTreeItem *)item withUniqueIdentifier:(NSString *)uniqueId beforeFetchLimit:(NSUInteger)fetchLimitBefore afterFetchLimit:(NSUInteger)fetchLimitAfter limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (^)(NSArray *entries))completionBlock { [self warmProcessIfNeeded]; __weak typeof(self) weakSelf = self; [[self remoteObjectProxy] fetchEntriesForView:item.uniqueIdentifier withUniqueIdentifier:uniqueId beforeFetchLimit:fetchLimitBefore afterFetchLimit:fetchLimitAfter limitToDate:limitToDate withCompletionBlock:^(NSArray *entries) { NSArray *logLines = [weakSelf _logLinesFromXPCObjects:entries]; completionBlock(logLines); }]; } - (void)fetchEntriesForItem:(IRCTreeItem *)item beforeUniqueIdentifier:(NSString *)uniqueId fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (^)(NSArray *entries))completionBlock { [self warmProcessIfNeeded]; __weak typeof(self) weakSelf = self; [[self remoteObjectProxy] fetchEntriesForView:item.uniqueIdentifier beforeUniqueIdentifier:uniqueId fetchLimit:fetchLimit limitToDate:limitToDate withCompletionBlock:^(NSArray *entries) { NSArray *logLines = [weakSelf _logLinesFromXPCObjects:entries]; completionBlock(logLines); }]; } - (void)fetchEntriesForItem:(IRCTreeItem *)item afterUniqueIdentifier:(NSString *)uniqueId fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (^)(NSArray *entries))completionBlock { [self warmProcessIfNeeded]; __weak typeof(self) weakSelf = self; [[self remoteObjectProxy] fetchEntriesForView:item.uniqueIdentifier afterUniqueIdentifier:uniqueId fetchLimit:fetchLimit limitToDate:limitToDate withCompletionBlock:^(NSArray *entries) { NSArray *logLines = [weakSelf _logLinesFromXPCObjects:entries]; completionBlock(logLines); }]; } - (void)fetchEntriesForItem:(IRCTreeItem *)item afterUniqueIdentifier:(NSString *)uniqueIdAfter beforeUniqueIdentifier:(NSString *)uniqueIdBefore fetchLimit:(NSUInteger)fetchLimit withCompletionBlock:(void (^)(NSArray *entries))completionBlock { [self warmProcessIfNeeded]; __weak typeof(self) weakSelf = self; [[self remoteObjectProxy] fetchEntriesForView:item.uniqueIdentifier afterUniqueIdentifier:uniqueIdAfter beforeUniqueIdentifier:uniqueIdBefore fetchLimit:fetchLimit withCompletionBlock:^(NSArray *entries) { NSArray *logLines = [weakSelf _logLinesFromXPCObjects:entries]; completionBlock(logLines); }]; } - (void)saveData { if (self.isTerminating) { if (self.processLoaded == NO && self.processLoading == NO) { return; } } if (self.isSaving == NO) { self.isSaving = YES; } else { LogToConsoleDebug("Cancelled save because a save is already saving"); return; } [self warmProcessIfNeeded]; [[self remoteObjectProxy] saveDataWithCompletionBlock:^{ self.isSaving = NO; if (self.isTerminating) { [self invalidateProcess]; } }]; } - (void)forgetItem:(IRCTreeItem *)item { [self warmProcessIfNeeded]; [[self remoteObjectProxy] forgetView:item.uniqueIdentifier]; } - (void)resetDataForItem:(IRCTreeItem *)item { [self warmProcessIfNeeded]; [[self remoteObjectProxy] resetDataForView:item.uniqueIdentifier]; } - (void)writeNewEntryWithLogLine:(TVCLogLine *)logLine forItem:(IRCTreeItem *)item { [self warmProcessIfNeeded]; TVCLogLineXPC *newEntry = [logLine xpcObjectForTreeItem:item]; [[self remoteObjectProxy] writeLogLine:newEntry]; } #pragma mark - #pragma mark Private API (Client) - (void)willDeleteUniqueIdentifiers:(NSArray *)uniqueIdentifiers inView:(NSString *)viewId { IRCTreeItem *item = [worldController() findItemWithId:viewId]; if (item == nil) { return; } [item.viewController notifyHistoricLogWillDeleteLines:uniqueIdentifiers]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/Extras/TVCLogControllerInlineMediaService.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentProtocol.h" #import "ICLPayload.h" #import "TXMasterController.h" #import "IRCClient.h" #import "IRCClientConfig.h" #import "IRCConnectionConfig.h" #import "IRCTreeItem.h" #import "IRCWorld.h" #import "TLOLocalization.h" #import "TPCPathInfoPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TDCPreferencesControllerPrivate.h" #import "TVCAlert.h" #import "TVCLogControllerPrivate.h" #import "TVCLogControllerInlineMediaServicePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCLogControllerInlineMediaService () @property (nonatomic, strong) NSXPCConnection *serviceConnection; @end @implementation TVCLogControllerInlineMediaService + (TVCLogControllerInlineMediaService *)sharedInstance { static id sharedSelf = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedSelf = [[self alloc] init]; }); return sharedSelf; } #pragma mark - #pragma mark Construction - (void)warmProcessIfNeeded { if (self.serviceConnection != nil) { return; } LogToConsoleDebug("Warming process..."); [self connectToService]; } - (void)invalidateProcess { if (self.serviceConnection == nil) { return; } LogToConsoleDebug("Invalidating process..."); [self.serviceConnection invalidate]; } - (void)connectToService { NSXPCConnection *serviceConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.codeux.app-utilities.Textual-InlineContentLoader"]; NSXPCInterface *remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ICLInlineContentServerProtocol)]; [remoteObjectInterface setClasses:[NSSet setWithObjects:[NSArray class], [NSURL class], nil] forSelector:@selector(warmServiceByLoadingPluginsAtLocations:) argumentIndex:0 ofReply:NO]; serviceConnection.remoteObjectInterface = remoteObjectInterface; NSXPCInterface *exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ICLInlineContentClientProtocol)]; serviceConnection.exportedInterface = exportedInterface; serviceConnection.exportedObject = self; serviceConnection.interruptionHandler = ^{ [self interruptionHandler]; LogToConsole("Interruption handler called"); }; serviceConnection.invalidationHandler = ^{ [self invalidationHandler]; LogToConsole("Invalidation handler called"); }; [serviceConnection resume]; self.serviceConnection = serviceConnection; [self registerDefaults]; [self registerPlugins]; } - (void)interruptionHandler { [self invalidateProcess]; } - (void)invalidationHandler { self.serviceConnection = nil; } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Invalidating media service process"); [self invalidateProcess]; } - (void)registerDefaults { /* We pass the registered defaults for the app to the XPC service because it accesses preferences within that domain. */ /* The registered defaults aren't changed after launch which means this is a one off deal, but we should use notifications if that ever changes in the future. */ NSDictionary *defaults = [RZUserDefaults() registeredDefaults]; [[self remoteObjectProxy] warmServiceByRegisteringDefaults:defaults]; } - (void)registerPlugins { NSArray *pluginLocations = @[ [self _applicationSupportInlineMediaPluginsURL] ]; [[self remoteObjectProxy] warmServiceByLoadingPluginsAtLocations:pluginLocations]; } - (NSURL *)_applicationSupportInlineMediaPluginsURL { NSURL *sourceURL = [TPCPathInfo groupContainerApplicationSupportURL]; NSURL *baseRL = [sourceURL URLByAppendingPathComponent:@"/Inline Media Modules/"]; [TPCPathInfo _createDirectoryAtURL:baseRL]; return baseRL; } #pragma mark - #pragma mark Private API - (id )remoteObjectProxy { return [self remoteObjectProxyWithErrorHandler:nil]; } - (id )remoteObjectProxyWithErrorHandler:(void (^ _Nullable)(NSError *error))handler { return [self.serviceConnection remoteObjectProxyWithErrorHandler:^(NSError *error) { LogToConsoleError("Error occurred while communicating with service: %{public}@", error.localizedDescription); if (handler) { handler(error); } }]; } #pragma mark - #pragma mark Public API - (void)processAddress:(NSString *)address withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index forItem:(IRCTreeItem *)item { NSParameterAssert(address != nil); NSParameterAssert(uniqueIdentifier != nil); NSParameterAssert(lineNumber != nil); NSParameterAssert(item != nil); /* WebKit is able to translate an address to punycode for us by giving it a pasteboard with the URL. */ NSURL *url = address.URLUsingWebKitPasteboard; if (url == nil) { LogToConsoleError("Address could not be normalized"); return; } [self processURL:url withUniqueIdentifier:uniqueIdentifier atLineNumber:lineNumber index:index forItem:item]; } - (void)processURL:(NSURL *)url withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index forItem:(IRCTreeItem *)item { NSParameterAssert(url != nil); NSParameterAssert(uniqueIdentifier != nil); NSParameterAssert(lineNumber != nil); NSParameterAssert(item != nil); [self warmProcessIfNeeded]; [[self remoteObjectProxy] processURL:url withUniqueIdentifier:uniqueIdentifier atLineNumber:lineNumber index:index inView:item.uniqueIdentifier]; } - (void)reloadService { [self invalidateProcess]; } #pragma mark - #pragma mark Private API (Client) - (void)processingPayloadSucceeded:(ICLPayload *)payload { IRCTreeItem *item = [worldController() findItemWithId:payload.viewIdentifier]; if (item == nil) { return; } [self _processingPayloadSucceeded:payload forItem:item]; } - (void)processingPayload:(ICLPayload *)payload failedWithError:(NSError *)error { IRCTreeItem *item = [worldController() findItemWithId:payload.viewIdentifier]; if (item == nil) { return; } [self _processingPayload:payload forItem:item failedWithError:error]; } - (void)_processingPayloadSucceeded:(ICLPayload *)payload forItem:(IRCTreeItem *)item { [item.viewController processingInlineMediaPayloadSucceeded:payload]; } - (void)_processingPayload:(ICLPayload *)payload forItem:(IRCTreeItem *)item failedWithError:(NSError *)error { [item.viewController processingInlineMediaPayload:payload failedWithError:error]; } #pragma mark - #pragma mark Helpers + (void)askPermissionToEnableInlineMediaWithCompletionBlock:(void (NS_NOESCAPE ^)(BOOL granted))completionBlock { BOOL presentDialog = NO; for (IRCClient *u in worldController().clientList) { if (u.config.proxyType != IRCConnectionProxyTypeNone) { presentDialog = YES; break; } } if (presentDialog == NO) { completionBlock(YES); return; } TVCAlert *alert = [TVCAlert new]; alert.messageText = TXTLS(@"Prompts[82q-zi]"); alert.informativeText = TXTLS(@"Prompts[vcq-sz]"); alert.type = TVCAlertTypeWarning; [alert setTitle:TXTLS(@"Prompts[xkj-nw]") forButton:TVCAlertResponseButtonFirst]; [alert setTitle:TXTLS(@"Prompts[qso-2g]") forButton:TVCAlertResponseButtonSecond]; [alert setTitle:TXTLS(@"Prompts[x3e-ur]") forButton:TVCAlertResponseButtonThird]; [alert setButtonClickedBlock:^BOOL(TVCAlert *sender, TVCAlertResponseButton buttonClicked) { [TDCPreferencesController openProxySettingsInSystemPreferences]; return NO; } forButton:TVCAlertResponseButtonThird]; TVCAlertResponseButton response = [alert runModal]; completionBlock(response == TVCAlertResponseButtonFirst); } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/Extras/TVCLogControllerOperationQueue.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" #import "IRCClient.h" #import "IRCChannel.h" #import "TVCLogController.h" #import "TVCLogControllerOperationQueuePrivate.h" NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Define Private Header @interface TVCLogControllerPrintingOperation : NSOperation @property (nonatomic, copy, nullable) TVCLogControllerPrintingBlock executionBlock; @property (nonatomic, weak) TVCLogController *viewController; @property (readonly, getter=isPending) BOOL pending; @property (nonatomic, assign, getter=isStandalone) BOOL standalone; @end @interface TVCLogControllerPrintingOperationQueue () /* This queue is application wide and stores all printing operations. Having a single queue gives us greater control over what happens, instead of trying to optimize one queue per-server. */ /* One problem of having a single queue through, is indexing which operations are associated with which view controller. To make this task easier, we maintain our own internal cache of pending operations which we can query at any time to know whats happening. The queue then observes the isFinished property to know when to remove the operations from our internal cache. */ @property (nonatomic, strong) NSMutableDictionary *pendingOperations; @end #pragma mark - #pragma mark Operation Queue @implementation TVCLogControllerPrintingOperationQueue - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; self.name = @"TVCLogControllerPrintingOperationQueue"; self.pendingOperations = [NSMutableDictionary dictionary]; self.qualityOfService = NSQualityOfServiceDefault; } #pragma mark - #pragma mark Queue Additions - (void)enqueueMessageBlock:(TVCLogControllerPrintingBlock)callbackBlock for:(TVCLogController *)viewController { [self enqueueMessageBlock:callbackBlock for:viewController isStandalone:NO]; } - (void)enqueueMessageBlock:(TVCLogControllerPrintingBlock)callbackBlock for:(TVCLogController *)viewController isStandalone:(BOOL)isStandalone { NSParameterAssert(callbackBlock != nil); NSParameterAssert(viewController != nil); if (masterController().applicationIsTerminating) { return; } TVCLogControllerPrintingOperation *operation = [TVCLogControllerPrintingOperation new]; operation.executionBlock = callbackBlock; operation.standalone = isStandalone; operation.viewController = viewController; [self addPendingOperation:operation]; } #pragma mark - #pragma mark Internal Operation Management - (NSArray *)pendingOperationsForViewController:(TVCLogController *)viewController { NSParameterAssert(viewController != nil); NSString *pendingOperationsKey = viewController.description; NSArray *pendingOperations = nil; @synchronized (self.pendingOperations) { pendingOperations = self.pendingOperations[pendingOperationsKey]; } if (pendingOperations == nil) { return @[]; } return [pendingOperations copy]; } - (void)addPendingOperation:(TVCLogControllerPrintingOperation *)operation { NSParameterAssert(operation != nil); TVCLogControllerPrintingOperation *operationDependency = nil; /* Add operation to list of pending operations and while we have those, also pick out what will be its dependency. */ NSString *pendingOperationsKey = operation.viewController.description; @synchronized (self.pendingOperations) { NSMutableArray *pendingOperations = self.pendingOperations[pendingOperationsKey]; if (pendingOperations == nil) { pendingOperations = [NSMutableArray array]; self.pendingOperations[pendingOperationsKey] = pendingOperations; } else { for (TVCLogControllerPrintingOperation *pendingOperation in pendingOperations.reverseObjectEnumerator) { if (pendingOperation.isCancelled || pendingOperation.isStandalone) { continue; } operationDependency = pendingOperation; break; } } [pendingOperations addObject:operation]; } /* Add dependency to operation */ if (operationDependency) { [operation addDependency:operationDependency]; } /* Begin observing when the status of the operation changes */ [operation addObserver:self forKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew context:nil]; /* Add operation to the queue */ [super addOperation:operation]; } - (void)removePendingOperation:(TVCLogControllerPrintingOperation *)operation { NSParameterAssert(operation != nil); /* Remove operation from list of pending operations */ NSString *pendingOperationsKey = operation.viewController.description; @synchronized (self.pendingOperations) { NSMutableArray *pendingOperations = self.pendingOperations[pendingOperationsKey]; if (pendingOperations == nil) { LogToConsoleError("'pendingOperations' is nil when it's not supposed to be. wat?"); return; } [pendingOperations removeObjectIdenticalTo:operation]; } /* Remove dependency to operation */ /* Having a ton of chained dependencies can cause a stack overflow when they deallocate. Manually removing dependencies fixes this pattern: 0 CoreFoundation 0x00007fffa2694d1e -[__NSArrayM dealloc] + 14 1 Foundation 0x00007fffa410d79b -[__NSOperationInternal dealloc] + 177 2 Foundation 0x00007fffa410d664 -[NSOperation dealloc] + 58 3 CoreFoundation 0x00007fffa27e237e common_removeAllObjects + 254 4 CoreFoundation 0x00007fffa2694d23 -[__NSArrayM dealloc] + 19 5 Foundation 0x00007fffa410d79b -[__NSOperationInternal dealloc] + 177 6 Foundation 0x00007fffa410d664 -[NSOperation dealloc] + 58 7 CoreFoundation 0x00007fffa27e237e common_removeAllObjects + 254 ... repeated for a total of 512 hops before crashing. */ NSOperation *operationDependency = operation.dependencies.firstObject; if (operationDependency) { [operation removeDependency:operationDependency]; } /* End observing when the status of the operation changes */ [operation removeObserver:self forKeyPath:@"isFinished"]; } #pragma mark - #pragma mark cancelAllOperations Substitute - (void)cancelOperationsForViewController:(TVCLogController *)viewController { NSParameterAssert(viewController != nil); NSArray *operations = [self pendingOperationsForViewController:viewController]; [operations makeObjectsPerformSelector:@selector(cancel)]; } - (void)cancelOperationsForClient:(IRCClient *)client { NSParameterAssert(client != nil); [self cancelOperationsForViewController:client.viewController]; } - (void)cancelOperationsForChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); [self cancelOperationsForViewController:channel.viewController]; } #pragma mark - #pragma mark State Changes - (void)updateReadinessState:(TVCLogController *)viewController { NSParameterAssert(viewController != nil); NSArray *operations = [self pendingOperationsForViewController:viewController]; for (TVCLogControllerPrintingOperation *operation in operations) { if (operation.isPending == NO || operation.dependencies.count > 0) { continue; } [operation willChangeValueForKey:@"isReady"]; [operation didChangeValueForKey:@"isReady"]; } } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:@"isFinished"]) { [self removePendingOperation:object]; } } @end #pragma mark - #pragma mark Operation Queue Items @implementation TVCLogControllerPrintingOperation - (BOOL)isPending { return (self.isCancelled == NO && self.isExecuting == NO && self.isFinished == NO); } - (void)main { [self executeBlock]; } - (void)executeBlock { if (self.isCancelled) { return; } self.executionBlock(self); } - (BOOL)isReady { if (self.dependencies.count < 1 || self.isStandalone) { return (super.isReady && self.viewController.viewIsLoaded); } else { return super.isReady; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCLogController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXGlobalModels.h" #import "TXMasterController.h" #import "TXMenuControllerPrivate.h" #import "ICLPayloadLocalPrivate.h" #import "IRCClientConfig.h" #import "IRCClientPrivate.h" #import "IRCChannel.h" #import "THOPluginDispatcherPrivate.h" #import "THOPluginManagerPrivate.h" #import "THOPluginProtocolPrivate.h" #import "TPCPathInfo.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TPCThemeController.h" #import "TPCThemePrivate.h" #import "TLOLinkParser.h" #import "TLOLocalization.h" #import "TVCLogViewPrivate.h" #import "TVCLogLine.h" #import "TVCLogRenderer.h" #import "TVCLogControllerHistoricLogFilePrivate.h" #import "TVCLogControllerInlineMediaServicePrivate.h" #import "TVCLogControllerOperationQueuePrivate.h" #import "TVCMainWindowPrivate.h" #import "TVCLogControllerPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _enqueueBlock(operationBlock) \ [self.printingQueue enqueueMessageBlock:(operationBlock) for:self isStandalone:NO]; #define _enqueueBlockStandalone(operationBlock) \ [self.printingQueue enqueueMessageBlock:(operationBlock) for:self isStandalone:YES]; @interface TVCLogControllerPrintOperationContext () @property (nonatomic, weak, readwrite) IRCClient *client; @property (nonatomic, weak, readwrite) IRCChannel *channel; @property (nonatomic, assign, readwrite, getter=isHighlight) BOOL highlight; @property (nonatomic, copy, readwrite) TVCLogLine *logLine; @property (nonatomic, copy, readwrite) NSString *lineNumber; @end @interface TVCLogController () @property (nonatomic, assign, readwrite, getter=viewIsLoaded) BOOL loaded; @property (nonatomic, assign) BOOL terminating; @property (nonatomic, assign) BOOL historyLoadedForFirstTime; @property (nonatomic, assign) BOOL reloadingHistory; @property (nonatomic, assign) BOOL reloadingTheme; @property (nonatomic, assign) BOOL historyLoaded; @property (nonatomic, assign) NSInteger activeLineCount; @property (nonatomic, copy, nullable) NSString *lastVisitedHighlight; @property (nonatomic, copy, nullable, readwrite) NSString *newestLineNumberFromPreviousSession; @property (nonatomic, copy, nullable, readwrite) NSString *oldestLineNumber; @property (nonatomic, copy, nullable, readwrite) NSString *newestLineNumber; @property (nonatomic, strong, nullable) TVCLogLine *lastLine; @property (nonatomic, strong) NSMutableArray *highlightedLineNumbers; @property (nonatomic, strong) NSCache *jumpToLineCallbacks; @property (nonatomic, strong, readwrite) TVCLogView *backingView; @property (weak, readonly) IRCTreeItem *associatedItem; @property (nonatomic, weak, readwrite) IRCClient *associatedClient; @property (nonatomic, weak, readwrite) IRCChannel *associatedChannel; @property (nonatomic, weak, readwrite) TVCMainWindow *attachedWindow; @property (nonatomic, assign) NSTimeInterval viewLoadedTimestamp; @property (readonly) TVCLogControllerPrintingOperationQueue *printingQueue; @property (readonly, copy) NSURL *baseURL; @end NSString * const TVCLogControllerViewFinishedLoadingNotification = @"TVCLogControllerViewFinishedLoadingNotification"; @implementation TVCLogController #pragma mark - #pragma mark Initialization - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithClient:(IRCClient *)client inWindow:(TVCMainWindow *)window { NSParameterAssert(client != nil); NSParameterAssert(window != nil); if ((self = [super init])) { self.associatedClient = client; self.attachedWindow = window; [self prepareInitialState]; [self setUp]; return self; } return nil; } - (instancetype)initWithChannel:(IRCChannel *)channel inWindow:(TVCMainWindow *)window { NSParameterAssert(channel != nil); NSParameterAssert(window != nil); if ((self = [super init])) { self.associatedClient = channel.associatedClient; self.associatedChannel = channel; self.attachedWindow = window; [self prepareInitialState]; [self setUp]; return self; } return nil; } - (void)prepareInitialState { #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 self.encrypted = self.associatedChannel.encryptionStateIsEncrypted; #endif self.highlightedLineNumbers = [NSMutableArray new]; self.jumpToLineCallbacks = [NSCache new]; } - (void)prepareForTermination:(BOOL)isTerminatingApplication { self.terminating = YES; self.loaded = NO; [self.backingView stopLoading]; // allow view to teardown self.backingView = nil; [self.printingQueue cancelOperationsForViewController:self]; if (isTerminatingApplication) { [self closeHistoricLog]; } else { [self historicLogForgetChannel]; } } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Preparing view controller: %{public}@", self.uniqueIdentifier); [self prepareForTermination:YES]; } - (void)prepareForPermanentDestruction { [self prepareForTermination:NO]; } - (void)dealloc { [self cancelPerformRequests]; } #pragma mark - #pragma mark Create View - (void)setUp { [self buildBackingView]; [self loadInitialDocument]; } - (void)buildBackingView { self.backingView = [[TVCLogView alloc] initWithViewController:self]; } - (void)rebuildBackingView { [self buildBackingView]; if (self.visible) { [self.attachedWindow updateChannelViewBoxContentViewSelection]; } } - (void)loadInitialDocument { [self loadAlternateHTML:[self initialDocument]]; } - (void)loadAlternateHTML:(NSString *)newHTML { NSParameterAssert(newHTML != nil); [self.backingView stopLoading]; [self.backingView loadHTMLString:newHTML baseURL:self.baseURL]; } #pragma mark - #pragma mark Manage Historic Log - (void)historicLogForgetChannel { /* Delete any trace of the channel, including context */ [TVCLogControllerHistoricLogSharedInstance() forgetItem:self.associatedItem]; } - (void)historicLogResetChannel { /* Delete log for channel but keep context */ [TVCLogControllerHistoricLogSharedInstance() resetDataForItem:self.associatedItem]; } - (void)closeHistoricLog { /* The historic log file is always open regardless of whether the user asked Textual to remember the history between restarts. It is always open because the reloading of a theme uses it to fill in the backlog after a reload. -closeHistoricLog is the point where we decide to actually save the file or erase it. If the user has Textual configured to remember between restarts, then we call a save before terminating. Or, we just erase the file from the path that it is written to entirely. */ IRCChannel *channel = self.associatedChannel; if ( /* 1 */ [TPCPreferences reloadScrollbackOnLaunch] == NO || /* 2 */ channel.isUtility || /* 3 */ (channel.isPrivateMessage && [TPCPreferences rememberServerListQueryStates] == NO) || /* 4 */ self.encrypted) { [self historicLogResetChannel]; } } #pragma mark - #pragma mark Properties - (nullable IRCTreeItem *)associatedItem { if (self.associatedChannel) { return self.associatedChannel; } else { return self.associatedClient; } } - (NSString *)uniqueIdentifier { return self.associatedItem.uniqueIdentifier; } - (NSURL *)baseURL { return themeController().temporaryURL; } - (TVCLogControllerPrintingOperationQueue *)printingQueue { return [TXSharedApplication sharedPrintingQueue]; } - (BOOL)inlineMediaEnabledForView { IRCChannel *channel = self.associatedChannel; if (channel == nil) { return NO; } IRCChannelConfig *config = channel.config; return (([TPCPreferences showInlineMedia] && config.inlineMediaDisabled == NO) || ([TPCPreferences showInlineMedia] == NO && config.inlineMediaEnabled)); } - (BOOL)viewIsSelected { return (self.attachedWindow.selectedViewController == self); } - (BOOL)viewIsVisible { IRCChannel *channel = self.associatedChannel; if (channel) { return [self.attachedWindow isItemVisible:channel]; } else { return [self.attachedWindow isItemVisible:self.associatedClient]; } } #pragma mark - #pragma mark Document Append & JavaScript Controller - (void)evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments { [self evaluateFunction:function withArguments:arguments onQueue:YES]; } - (void)evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments onQueue:(BOOL)onQueue { NSParameterAssert(function != nil); if (self.terminating) { return; } if (onQueue) { TVCLogControllerPrintingBlock scriptBlock = ^(id operation) { [self _evaluateFunction:function withArguments:arguments]; }; _enqueueBlock(scriptBlock) } else { [self _evaluateFunction:function withArguments:arguments]; } } - (void)_evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments { NSParameterAssert(function != nil); if (self.loaded == NO || self.terminating) { return; } [self.backingView evaluateFunction:function withArguments:arguments]; } - (void)appendToDocumentBody:(NSString *)html withLineNumbers:(NSArray *)lineNumbers { NSParameterAssert(html != nil); [self _evaluateFunction:@"MessageBuffer.bufferElementAppend" withArguments:@[html, lineNumbers]]; } #pragma mark - #pragma mark Channel Topic Bar - (void)setInitialTopic { NSString *topic = self.associatedChannel.topic; [self setTopic:topic]; } - (void)setTopic:(nullable NSString *)topic { if (self.terminating) { return; } TVCLogControllerPrintingBlock operationBlock = ^(id operation) { NSString *topicString = nil; if (topic == nil || topic.length == 0) { topicString = TXTLS(@"TVCMainWindow[vi3-23]"); } else { topicString = topic; } NSString *topicTemplate = [TVCLogRenderer renderBody:topicString forViewController:self withAttributes:@{ TVCLogRendererConfigurationRenderLinksAttribute : @YES, TVCLogRendererConfigurationLineTypeAttribute : @(TVCLogLineTypeTopic) } resultInfo:NULL]; [self _evaluateFunction:@"Textual.setTopicBarValue" withArguments:@[topicString, topicTemplate]]; [self.backingView redrawView]; }; _enqueueBlockStandalone(operationBlock) } #pragma mark - #pragma mark Move to Bottom/Top - (void)moveToTop { [self _evaluateFunction:@"Textual.scrollToTopOfView" withArguments:@[@(YES)]]; } - (void)moveToBottom { [self _evaluateFunction:@"Textual.scrollToBottomOfView" withArguments:@[@(YES)]]; } #pragma mark - #pragma mark Add/Remove History Mark - (void)mark { TVCLogControllerPrintingBlock operationBlock = ^(id operation) { NSString *markTemplate = [TVCLogRenderer renderTemplateNamed:@"historyIndicator"]; [self _evaluateFunction:@"_Textual.historyIndicatorAdd" withArguments:@[markTemplate]]; }; _enqueueBlock(operationBlock); } - (void)unmark { [self _evaluateFunction:@"_Textual.historyIndicatorRemove" withArguments:nil]; } - (void)goToMark { [self _evaluateFunction:@"Textual.scrollToHistoryIndicator" withArguments:nil]; } #pragma mark - #pragma mark Reload Scrollback - (void)appendHistoricMessageFragment:(NSString *)html withLineNumbers:(NSArray *)lineNumbers isReload:(BOOL)isReload { NSParameterAssert(html != nil); [self _evaluateFunction:@"_Textual.documentBodyAppendHistoric" withArguments:@[html, lineNumbers, @(isReload)]]; } /* reloadOldLines: is supposed to be called from inside a queue. */ - (void)reloadOldLines:(NSArray *)oldLines isReload:(BOOL)isReload { NSParameterAssert(oldLines != nil); NSMutableArray *lineNumbers = [NSMutableArray array]; NSMutableString *patchedAppend = [NSMutableString string]; NSMutableArray *pluginObjects = nil; for (TVCLogLine *logLine in oldLines) { /* Render result info HTML */ NSDictionary *resultInfo = nil; NSString *html = [self renderLogLine:logLine resultInfo:&resultInfo]; if (html == nil) { LogToConsoleError("Failed to render log line %{public}@", logLine.description); continue; } [patchedAppend appendString:html]; /* Record information about rendering */ NSString *lineNumber = logLine.uniqueIdentifier; [lineNumbers addObject:lineNumber]; /* Add reference to plugin concrete object */ THOPluginDidPostNewMessageConcreteObject *pluginObject = resultInfo[@"pluginConcreteObject"]; if (pluginObject) { if (pluginObjects == nil) { pluginObjects = [NSMutableArray array]; } [pluginObjects addObject:pluginObject]; } /* Record highlights */ BOOL highlighted = [resultInfo boolForKey:TVCLogRendererResultsKeywordMatchFoundAttribute]; if (highlighted) { @synchronized(self.highlightedLineNumbers) { [self.highlightedLineNumbers addObject:lineNumber]; } } } /* Render the result in WebKit */ [self appendHistoricMessageFragment:patchedAppend withLineNumbers:lineNumbers isReload:isReload]; /* Inform plugins of new content */ for (THOPluginDidPostNewMessageConcreteObject *pluginObject in pluginObjects) { pluginObject.isProcessedInBulk = YES; [THOPluginDispatcher enqueueDidPostNewMessage:pluginObject]; } } - (void)maybeReloadHistory { if (self.loaded == NO) { return; } if (self.historyLoaded) { return; } [self reloadHistory]; } - (void)reloadHistory { if (self.terminating) { return; } BOOL firstTimeLoadingHistory = (self.historyLoadedForFirstTime == NO); IRCChannel *channel = self.associatedChannel; if ( /* 1 */ self.encrypted || /* 2 */ (firstTimeLoadingHistory && [TPCPreferences reloadScrollbackOnLaunch] == NO) || /* 3 */ channel.isUtility || /* 4 */ (firstTimeLoadingHistory && channel.isPrivateMessage && [TPCPreferences rememberServerListQueryStates] == NO)) { self.historyLoadedForFirstTime = YES; self.historyLoaded = YES; [self notifyViewFinishedLoadingHistory]; return; } else { BOOL lazyLoadHistory = [RZUserDefaults() boolForKey:@"Optimizations -> Load History Lazily"]; if (lazyLoadHistory && self.visible == NO) { return; } } self.reloadingHistory = YES; void (^reloadBlock)(NSArray *) = ^(NSArray *objects) { TVCLogLine *lastLine = objects.lastObject; /* Only assign lastLine to self if there is none because if we do it when there is some, then there will be a big cluster fuck of incorrect date changes. */ if (self.lastLine == nil) { self.lastLine = lastLine; } if (firstTimeLoadingHistory) { NSString *newestLineNumber = lastLine.uniqueIdentifier; self.newestLineNumberFromPreviousSession = newestLineNumber; } [self reloadOldLines:objects isReload:(firstTimeLoadingHistory == NO)]; self.reloadingHistory = NO; self.historyLoaded = YES; self.historyLoadedForFirstTime = YES; [self notifyViewFinishedLoadingHistory]; [self.backingView redrawViewIfNeeded]; }; TVCLogControllerPrintingBlock operationBlock = ^(id operation) { NSDate *limitToDate = [NSDate dateWithTimeIntervalSince1970:self.viewLoadedTimestamp]; [TVCLogControllerHistoricLogSharedInstance() fetchEntriesForItem:self.associatedItem ascending:NO fetchLimit:100 limitToDate:limitToDate withCompletionBlock:^(NSArray *objects) { if ([operation isCancelled]) { return; } reloadBlock(objects.reverseObjectEnumerator.allObjects); }]; }; _enqueueBlockStandalone(operationBlock) } - (void)reloadTheme { XRPerformBlockAsynchronouslyOnMainQueue(^{ [self _reloadTheme]; }); } - (void)_reloadTheme { if (self.terminating) { return; } if (self.reloadingTheme) { return; } /* Even if the user has never loaded their history, we force this flag to YES when reloading theme. We do not want history to be displayed as historic when playing it back after a reload. */ self.historyLoadedForFirstTime = YES; self.reloadingTheme = YES; [self clearWithReset:NO]; self.reloadingTheme = NO; } - (NSString *)dateIndicatorWithDate:(NSDate *)date { NSParameterAssert(date != nil); NSString *dateString = TXFormatDate(date, NSDateFormatterLongStyle, NSDateFormatterNoStyle, NO); return dateString; } #pragma mark - #pragma mark Utilities - (void)jumpToCurrentSession { NSString *lineNumber = self.newestLineNumberFromPreviousSession; if (lineNumber == nil) { lineNumber = self.oldestLineNumber; } if (lineNumber == nil) { return; } [self jumpToLine:lineNumber]; } - (void)jumpToPresent { NSString *lineNumber = self.newestLineNumber; if (lineNumber == nil) { lineNumber = self.newestLineNumberFromPreviousSession; } if (lineNumber == nil) { return; } [self jumpToLine:lineNumber]; } - (void)jumpToLine:(NSString *)lineNumber { [self jumpToLine:lineNumber completionHandler:nil]; } - (void)jumpToLine:(NSString *)lineNumber completionHandler:(void (^ _Nullable)(BOOL result))completionHandler { NSParameterAssert(lineNumber != nil); /* Jumping to line chains callback functions which may take time to load. We do not want invoke the completion handler until we know for certain whether the line was jumped to. We therefore change the completion handler and call it from a bridged function when we are finished. */ if (completionHandler) { [self.jumpToLineCallbacks setObject:completionHandler forKey:lineNumber]; } [self.backingView evaluateFunction:@"Textual.jumpToLine" withArguments:@[lineNumber]]; } - (void)notifyDidBecomeVisible /* When the view is switched to */ { [self _evaluateFunction:@"_Textual.notifyDidBecomeVisible" withArguments:nil]; [self maybeReloadHistory]; [self.backingView restoreScrollerPosition]; [self.backingView enableOffScreenUpdates]; [self.backingView redrawViewIfNeeded]; } - (void)notifySelectionChanged { [self _evaluateFunction:@"_Textual.notifySelectionChanged" withArguments:@[@(self.selected)]]; } - (void)notifyDidBecomeHidden { [self _evaluateFunction:@"_Textual.notifyDidBecomeHidden" withArguments:nil]; [self.backingView saveScrollerPosition]; [self.backingView disableOffScreenUpdates]; } - (void)notifyViewFinishedLoadingHistory { [self _evaluateFunction:@"_Textual.viewFinishedLoadingHistory" withArguments:nil]; } - (void)changeTextSize:(BOOL)bigger { double sizeMultiplier = self.attachedWindow.textSizeMultiplier; [self _evaluateFunction:@"Textual.changeTextSizeMultiplier" withArguments:@[@(sizeMultiplier)]]; [self _evaluateFunction:@"Textual.viewFontSizeChanged" withArguments:@[@(bigger)]]; } - (void)changeScrollbackLimit { NSUInteger scrollbackLimit = [TPCPreferences scrollbackVisibleLimit]; [self _evaluateFunction:@"_MessageBuffer.setBufferLimit" withArguments:@[@(scrollbackLimit)]]; } #pragma mark - #pragma mark Plugins - (void)notifyJumpToLine:(NSString *)lineNumber successful:(BOOL)successful scrolledToBottom:(BOOL)scrolledToBottom { NSParameterAssert(lineNumber != nil); /* The Objective-C based automatic scroller relies on notifications of bounds and frame changes to know when a WebView scrolls. If the WebView is offscreen, then we have no way to know when a jump occurs because these notifications are not received. To workaround this, the JavaScript passes the scrolledToBottom argument. The Objective-C automatic scroller can then be passed this argument to know whether to perform automatic scrolling when the view becomes visible. */ if (successful) { [self.backingView resetScrollerPositionTo:scrolledToBottom]; } void (^callbackHandler)(BOOL) = [self.jumpToLineCallbacks objectForKey:lineNumber]; if (callbackHandler == nil) { return; } /* Remove callback handler first incase the callback handler tries to jump to same line number again for some reason. */ [self.jumpToLineCallbacks removeObjectForKey:lineNumber]; callbackHandler(successful); } - (void)notifyLinesAddedToView:(NSArray *)lineNumbers { NSParameterAssert(lineNumbers != nil); if (self.loaded == NO || self.terminating) { return; } self.activeLineCount += lineNumbers.count; if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureNewMessagePostedEvent] == NO) { return; } for (NSString *lineNumber in lineNumbers) { [THOPluginDispatcher dequeueDidPostNewMessageWithLineNumber:lineNumber forViewController:self]; } } - (void)notifyLinesRemovedFromView:(NSArray *)lineNumbers { NSParameterAssert(lineNumbers != nil); if (self.loaded == NO || self.terminating) { return; } self.activeLineCount -= lineNumbers.count; } - (void)notifyHistoricLogWillDeleteLines:(NSArray *)lineNumbers { NSParameterAssert(lineNumbers != nil); /* It is possible for this method to be invoked before loaded is YES such as when performing a clear. */ if (/* self.loaded == NO || */ self.terminating) { return; } @synchronized(self.highlightedLineNumbers) { [self.highlightedLineNumbers removeObjectsInArray:lineNumbers]; } } #pragma mark - #pragma mark Inline Media - (void)processingInlineMediaPayloadSucceeded:(ICLPayload *)payload { [self _evaluateFunction:@"_InlineMediaLoader.processPayload" withArguments:@[payload.javaScriptObject]]; } - (void)processingInlineMediaPayload:(ICLPayload *)payload failedWithError:(NSError *)error { LogToConsoleError("Processing request for '%{public}@' at '%{public}@' failed with error: %{public}@", payload.uniqueIdentifier, payload.lineNumber, error.localizedDescription); } - (void)processInlineMedia:(NSArray *)mediaLinks atLineNumber:(NSString *)lineNumber { NSParameterAssert(mediaLinks != nil); NSParameterAssert(lineNumber != nil); if (mediaLinks.count == 0) { return; } /* Unique list */ NSMutableArray *linksMatched = [NSMutableArray array]; NSMutableArray *linksToProcess = [NSMutableArray array]; for (AHHyperlinkScannerResult *link in mediaLinks) { if ([linksMatched containsObject:link.stringValue]) { continue; } [linksToProcess addObject:link]; } [linksToProcess enumerateObjectsUsingBlock:^(AHHyperlinkScannerResult *link, NSUInteger index, BOOL *stop) { [self processInlineMediaAtAddress:link.stringValue withUniqueIdentifier:link.uniqueIdentifier atLineNumber:lineNumber index:index]; }]; } - (void)processInlineMediaAtAddress:(NSString *)address withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index { NSParameterAssert(address != nil); NSParameterAssert(uniqueIdentifier != nil); NSParameterAssert(lineNumber != nil); IRCTreeItem *associatedItem = self.associatedItem; [TVCLogControllerInlineMediaSharedInstance() processAddress:address withUniqueIdentifier:uniqueIdentifier atLineNumber:lineNumber index:index forItem:associatedItem]; } #pragma mark - #pragma mark Manage Highlights - (NSUInteger)numberOfLines { return self.activeLineCount; } - (BOOL)highlightAvailable:(BOOL)previous { if (self.loaded == NO || self.terminating) { return NO; } @synchronized(self.highlightedLineNumbers) { return (self.highlightedLineNumbers.count > 0); } } - (void)nextHighlight { if (self.loaded == NO || self.terminating) { return; } @synchronized(self.highlightedLineNumbers) { if (self.highlightedLineNumbers.count == 0) { return; } if ([self.highlightedLineNumbers containsObject:self.lastVisitedHighlight]) { NSUInteger hli_ci = [self.highlightedLineNumbers indexOfObject:self.lastVisitedHighlight]; if (hli_ci == (self.highlightedLineNumbers.count - 1)) { /* Circle around back to the beginning of the list. */ self.lastVisitedHighlight = self.highlightedLineNumbers[0]; } else { self.lastVisitedHighlight = self.highlightedLineNumbers[(hli_ci + 1)]; } } else { self.lastVisitedHighlight = self.highlightedLineNumbers[0]; } [self jumpToLine:self.lastVisitedHighlight]; } } - (void)previousHighlight { if (self.loaded == NO || self.terminating) { return; } @synchronized(self.highlightedLineNumbers) { if (self.highlightedLineNumbers.count == 0) { return; } if ([self.highlightedLineNumbers containsObject:self.lastVisitedHighlight]) { NSInteger hli_ci = [self.highlightedLineNumbers indexOfObject:self.lastVisitedHighlight]; if (hli_ci == 0) { /* Circle around back to the end of the list. */ self.lastVisitedHighlight = self.highlightedLineNumbers[(self.highlightedLineNumbers.count - 1)]; } else { self.lastVisitedHighlight = self.highlightedLineNumbers[(hli_ci - 1)]; } } else { self.lastVisitedHighlight = self.highlightedLineNumbers[0]; } [self jumpToLine:self.lastVisitedHighlight]; } } - (void)clearWithReset:(BOOL)clearWithReset { if (self.terminating) { return; } [self.printingQueue cancelOperationsForViewController:self]; if (clearWithReset) { [self historicLogResetChannel]; } @synchronized(self.highlightedLineNumbers) { [self.highlightedLineNumbers removeAllObjects]; } self.activeLineCount = 0; self.lastVisitedHighlight = nil; self.oldestLineNumber = nil; self.newestLineNumber = nil; self.lastLine = nil; self.loaded = NO; self.reloadingHistory = NO; self.historyLoaded = NO; if (self.backingView.isUsingWebKit2 != [TVCLogView webKit2Enabled]) { [self rebuildBackingView]; } [self loadInitialDocument]; } - (void)clear { if (self.terminating) { return; } [self clearWithReset:YES]; } - (void)clearBackingView { if (self.terminating) { return; } [self clearWithReset:YES]; } #pragma mark - #pragma mark History - (void)renderLogLinesBeforeLineNumber:(NSString *)lineNumber maximumNumberOfLines:(NSUInteger)maximumNumberOfLines completionBlock:(void (^)(NSArray *> *))completionBlock { [self _renderLogLinesAfter:NO lineNumber:lineNumber maximumNumberOfLines:maximumNumberOfLines completionBlock:completionBlock]; } - (void)renderLogLinesAfterLineNumber:(NSString *)lineNumber maximumNumberOfLines:(NSUInteger)maximumNumberOfLines completionBlock:(void (^)(NSArray *> *))completionBlock { [self _renderLogLinesAfter:YES lineNumber:lineNumber maximumNumberOfLines:maximumNumberOfLines completionBlock:completionBlock]; } - (void)_renderLogLinesAfter:(BOOL)after lineNumber:(NSString *)lineNumber maximumNumberOfLines:(NSUInteger)maximumNumberOfLines completionBlock:(void (^)(NSArray *> *))completionBlock { NSParameterAssert(lineNumber != nil); NSParameterAssert(maximumNumberOfLines > 0); NSParameterAssert(completionBlock != nil); TVCLogControllerPrintingBlock operationBlock = ^(id operation) { void (^historicLogCompletionBlock)(NSArray *) = ^(NSArray *entries) { if ([operation isCancelled]) { return; } [self _renderLogLinesAfterLineNumberPostFlight:entries completionBlock:completionBlock]; }; if (after == NO) { [TVCLogControllerHistoricLogSharedInstance() fetchEntriesForItem:self.associatedItem beforeUniqueIdentifier:lineNumber fetchLimit:maximumNumberOfLines limitToDate:nil withCompletionBlock:historicLogCompletionBlock]; } else { [TVCLogControllerHistoricLogSharedInstance() fetchEntriesForItem:self.associatedItem afterUniqueIdentifier:lineNumber fetchLimit:maximumNumberOfLines limitToDate:nil withCompletionBlock:historicLogCompletionBlock]; } }; _enqueueBlockStandalone(operationBlock) } - (void)renderLogLinesAfterLineNumber:(NSString *)lineNumberAfter beforeLineNumber:(NSString *)lineNumberBefore maximumNumberOfLines:(NSUInteger)maximumNumberOfLines completionBlock:(void (^)(NSArray *> * _Nonnull))completionBlock { NSParameterAssert(lineNumberAfter != nil); NSParameterAssert(lineNumberBefore != nil); NSParameterAssert(completionBlock != nil); TVCLogControllerPrintingBlock operationBlock = ^(id operation) { void (^historicLogCompletionBlock)(NSArray *) = ^(NSArray *entries) { if ([operation isCancelled]) { return; } [self _renderLogLinesAfterLineNumberPostFlight:entries completionBlock:completionBlock]; }; [TVCLogControllerHistoricLogSharedInstance() fetchEntriesForItem:self.associatedItem afterUniqueIdentifier:lineNumberAfter beforeUniqueIdentifier:lineNumberBefore fetchLimit:maximumNumberOfLines withCompletionBlock:historicLogCompletionBlock]; }; _enqueueBlockStandalone(operationBlock) } - (void)renderLogLineAtLineNumber:(NSString *)lineNumber numberOfLinesBefore:(NSUInteger)numberOfLinesBefore numberOfLinesAfter:(NSUInteger)numberOfLinesAfter completionBlock:(void (^)(NSArray *> * _Nonnull))completionBlock { NSParameterAssert(lineNumber != nil); NSParameterAssert(completionBlock != nil); TVCLogControllerPrintingBlock operationBlock = ^(id operation) { void (^historicLogCompletionBlock)(NSArray *) = ^(NSArray *entries) { if ([operation isCancelled]) { return; } [self _renderLogLinesAfterLineNumberPostFlight:entries completionBlock:completionBlock]; }; [TVCLogControllerHistoricLogSharedInstance() fetchEntriesForItem:self.associatedItem withUniqueIdentifier:lineNumber beforeFetchLimit:numberOfLinesBefore afterFetchLimit:numberOfLinesAfter limitToDate:nil withCompletionBlock:historicLogCompletionBlock]; }; _enqueueBlockStandalone(operationBlock) } - (void)_renderLogLinesAfterLineNumberPostFlight:(NSArray *)logLines completionBlock:(void (^)(NSArray *> *))completionBlock { NSParameterAssert(logLines != nil); NSParameterAssert(completionBlock != nil); NSMutableArray *> *renderedLogLines = [NSMutableArray arrayWithCapacity:logLines.count]; NSMutableArray *pluginObjects = nil; for (TVCLogLine *logLine in logLines) { /* Render result info HTML */ NSDictionary *resultInfo = nil; NSString *html = [self renderLogLine:logLine resultInfo:&resultInfo]; if (html == nil) { LogToConsoleError("Failed to render log line %{public}@", logLine.description); continue; } /* Record information about rendering */ NSString *lineNumber = logLine.uniqueIdentifier; [renderedLogLines addObject:@{ @"lineNumber" : lineNumber, @"html" : html, @"timestamp" : @(logLine.receivedAt.timeIntervalSince1970) }]; /* Add reference to plugin concrete object */ THOPluginDidPostNewMessageConcreteObject *pluginObject = resultInfo[@"pluginConcreteObject"]; if (pluginObject) { if (pluginObjects == nil) { pluginObjects = [NSMutableArray array]; } [pluginObjects addObject:pluginObject]; } } /* Inform plugins of new content */ for (THOPluginDidPostNewMessageConcreteObject *pluginObject in pluginObjects) { pluginObject.isProcessedInBulk = YES; [THOPluginDispatcher enqueueDidPostNewMessage:pluginObject]; } /* Finish */ completionBlock([renderedLogLines copy]); } #pragma mark - #pragma mark Print - (void)print:(TVCLogLine *)logLine { [self print:logLine completionBlock:NULL]; } - (void)print:(TVCLogLine *)logLine completionBlock:(nullable TVCLogControllerPrintOperationCompletionBlock)completionBlock { NSParameterAssert(logLine != nil); if (self.terminating) { return; } if ([logLine isKindOfClass:[TVCLogLineMutable class]]) { logLine = [logLine copy]; } self.lastLine = logLine; TVCLogControllerPrintingBlock printBlock = ^(id operation) { NSDictionary *resultInfo = nil; NSString *html = [self renderLogLine:logLine resultInfo:&resultInfo]; if (html == nil) { LogToConsoleError("Failed to render log line %{public}@", logLine.description); return; } NSString *lineNumber = logLine.uniqueIdentifier; NSSet *listOfUsers = resultInfo[TVCLogRendererResultsListOfUsersFoundAttribute]; BOOL processInlineMedia = [resultInfo boolForKey:@"processInlineMedia"]; BOOL highlighted = [resultInfo boolForKey:TVCLogRendererResultsKeywordMatchFoundAttribute]; THOPluginDidPostNewMessageConcreteObject *pluginObject = resultInfo[@"pluginConcreteObject"]; XRPerformBlockAsynchronouslyOnMainQueue(^{ if (self.terminating) { return; } if (self.oldestLineNumber == nil) { self.oldestLineNumber = lineNumber; } self.newestLineNumber = lineNumber; IRCClient *client = self.associatedClient; IRCChannel *channel = self.associatedChannel; if (highlighted) { @synchronized(self.highlightedLineNumbers) { [self.highlightedLineNumbers addObject:lineNumber]; } [client cacheHighlightInChannel:channel withLogLine:logLine]; } if (pluginObject) { [THOPluginDispatcher enqueueDidPostNewMessage:resultInfo[@"pluginConcreteObject"]]; } [self appendToDocumentBody:html withLineNumbers:@[lineNumber]]; /* TODO: Modify logic of inline media to only truly process images if the line is in fact on the WebView. */ /* Begin processing inline media */ /* We go through the inline media list here and pass to the loader now so that we know the links have hit the WebView before we even try loading them. */ if (processInlineMedia) { NSArray *listOfLinks = resultInfo[TVCLogRendererResultsListOfLinksInBodyAttribute]; [self processInlineMedia:listOfLinks atLineNumber:lineNumber]; } /* Log this log line */ /* If the channel is encrypted, then we refuse to write to the actual historic log so there is no trace of the chatter on the disk in the form of an unencrypted cache file. */ /* Doing it this way does break the ability to reload chatter in the view as well as playback on restart, but the added security can be seen as a bonus. */ if (self.encrypted == NO) { [TVCLogControllerHistoricLogSharedInstance() writeNewEntryWithLogLine:logLine forItem:self.associatedItem]; } /* Redraw view if needed */ [self.backingView redrawViewIfNeeded]; /* Using information provided by conversation tracking we can update our internal array of favored nicknames for nick completion. */ if (logLine.memberType == TVCLogLineMemberTypeLocalUser) { [listOfUsers.allObjects makeObjectsPerformSelector:@selector(outgoingConversation)]; } else { [listOfUsers.allObjects makeObjectsPerformSelector:@selector(conversation)]; } if (completionBlock == nil) { return; } TVCLogControllerPrintOperationContext *contextObject = [TVCLogControllerPrintOperationContext new]; contextObject.client = client; contextObject.channel = channel; contextObject.highlight = highlighted; contextObject.logLine = logLine; contextObject.lineNumber = lineNumber; completionBlock(contextObject); }); }; _enqueueBlock(printBlock) } - (nullable NSString *)renderLogLine:(TVCLogLine *)logLine resultInfo:(NSDictionary ** _Nullable)resultInfo { NSParameterAssert(logLine != nil); // ************************************************************************** / TVCLogLineType lineType = logLine.lineType; NSString *lineTypeString = logLine.lineTypeString; BOOL renderLinks = ([[TLOLinkParser bannedLineTypes] containsObject:lineTypeString] == NO); NSMutableDictionary *rendererAttributes = [NSMutableDictionary dictionary]; if (logLine.rendererAttributes != nil) { [rendererAttributes addEntriesFromDictionary:logLine.rendererAttributes]; } [rendererAttributes maybeSetObject:logLine.excludeKeywords forKey:TVCLogRendererConfigurationExcludedKeywordsAttribute]; [rendererAttributes maybeSetObject:logLine.highlightKeywords forKey:TVCLogRendererConfigurationHighlightKeywordsAttribute]; [rendererAttributes setBool:renderLinks forKey:TVCLogRendererConfigurationRenderLinksAttribute]; [rendererAttributes setUnsignedInteger:logLine.lineType forKey:TVCLogRendererConfigurationLineTypeAttribute]; [rendererAttributes setUnsignedInteger:logLine.memberType forKey:TVCLogRendererConfigurationMemberTypeAttribute]; NSDictionary *rendererResults = nil; NSString *renderedBody = [TVCLogRenderer renderBody:logLine.messageBody forViewController:self withAttributes:rendererAttributes resultInfo:&rendererResults]; if (renderedBody == nil) { return nil; } NSMutableDictionary *resultInfoTemp = nil; if (resultInfo) { resultInfoTemp = [rendererResults mutableCopy]; } BOOL highlighted = [rendererResults boolForKey:TVCLogRendererResultsKeywordMatchFoundAttribute]; BOOL inlineMedia = (self.inlineMediaEnabledForView && (lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypeAction)); NSString *lineNumber = logLine.uniqueIdentifier; NSDate *receivedAt = logLine.receivedAt; // ************************************************************************** / NSMutableDictionary *pathAttributes = [NSMutableDictionary new]; pathAttributes[@"activeStyleAbsolutePath"] = self.baseURL.path; pathAttributes[@"applicationResourcePath"] = [TPCPathInfo applicationResources]; NSMutableDictionary *templateAttributes = [pathAttributes mutableCopy]; // ---- // templateAttributes[@"timestamp"] = @(receivedAt.timeIntervalSince1970); templateAttributes[@"formattedTimestamp"] = logLine.formattedTimestamp; templateAttributes[@"localizedTimestamp"] = TXFormatDateLongStyle(receivedAt, NO); // ---- // NSString *nickname = [logLine formattedNicknameInChannel:self.associatedChannel]; if (nickname.length == 0) { templateAttributes[@"isNicknameAvailable"] = @(NO); } else { templateAttributes[@"isNicknameAvailable"] = @(YES); templateAttributes[@"nicknameColorStyle"] = logLine.nicknameColorStyle; templateAttributes[@"nicknameColorStyleOverride"] = @(logLine.nicknameColorStyleOverride); templateAttributes[@"nicknameColorHashingEnabled"] = @([TPCPreferences disableNicknameColorHashing] == NO); templateAttributes[@"formattedNickname"] = nickname.trim; templateAttributes[@"nickname"] = logLine.nickname; templateAttributes[@"nicknameType"] = logLine.memberTypeString; } // ---- // templateAttributes[@"lineType"] = lineTypeString; templateAttributes[@"command"] = logLine.command; templateAttributes[@"rawCommand"] = logLine.command; // Legacy key // ---- // NSString *classAttribute = nil; if (lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeNotice) { classAttribute = @"text"; } else { classAttribute = @"event"; } templateAttributes[@"lineClassAttribute"] = classAttribute; // ---- // if (highlighted) { templateAttributes[@"highlightAttribute"] = @"true"; } else { templateAttributes[@"highlightAttribute"] = @"false"; } // ---- // templateAttributes[@"message"] = logLine.messageBody; templateAttributes[@"formattedMessage"] = renderedBody; templateAttributes[@"isHighlight"] = @(highlighted); templateAttributes[@"isRemoteMessage"] = @(logLine.memberType == TVCLogLineMemberTypeNormal); // ---- // if (logLine.isEncrypted) { templateAttributes[@"isEncrypted"] = @(YES); } // ---- // NSString *serverName = self.associatedClient.networkNameAlt; if (serverName) { templateAttributes[@"configuredServerName"] = serverName; } // ---- // templateAttributes[@"inlineMediaEnabled"] = @(inlineMedia); templateAttributes[@"lineNumber"] = lineNumber; templateAttributes[@"lineRenderTime"] = @([NSDate timeIntervalSince1970]); // ---- // if (logLine.isFirstForDay && [TPCPreferences showDateChanges]) { templateAttributes[@"showDateIndicator"] = @(YES); templateAttributes[@"dateIndicatorMessage"] = [self dateIndicatorWithDate:receivedAt]; } if ([lineNumber isEqualToString:self.newestLineNumberFromPreviousSession]) { templateAttributes[@"showSessionIndicator"] = @(YES); templateAttributes[@"sessionIndicatorMessage"] = TXTLS(@"TVCMainWindow[4yo-mk]"); } // ************************************************************************** / if (resultInfoTemp) { resultInfoTemp[@"processInlineMedia"] = @(inlineMedia); if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureNewMessagePostedEvent]) { NSArray *listOfLinks = rendererResults[TVCLogRendererResultsListOfLinksInBodyAttribute]; THOPluginDidPostNewMessageConcreteObject *pluginConcreteObject = [THOPluginDidPostNewMessageConcreteObject new]; pluginConcreteObject.keywordMatchFound = highlighted; pluginConcreteObject.lineType = lineType; pluginConcreteObject.memberType = logLine.memberType; pluginConcreteObject.senderNickname = logLine.nickname; pluginConcreteObject.receivedAt = receivedAt; pluginConcreteObject.lineNumber = lineNumber; pluginConcreteObject.messageContents = rendererResults[TVCLogRendererResultsOriginalBodyWithoutEffectsAttribute]; pluginConcreteObject.listOfHyperlinks = listOfLinks; pluginConcreteObject.listOfUsers = rendererResults[TVCLogRendererResultsListOfUsersFoundAttribute]; resultInfoTemp[@"pluginConcreteObject"] = pluginConcreteObject; } } // ************************************************************************** / if ( resultInfo) { *resultInfo = [resultInfoTemp copy]; } // ************************************************************************** / GRMustacheTemplate *template = [theme() templateWithLineType:lineType]; NSString *html = [TVCLogRenderer renderTemplate:template attributes:templateAttributes]; return html; } #pragma mark - #pragma mark Initial Document - (BOOL)usesCustomScrollers { NSScrollerStyle preferredScrollerStyle = [NSScroller preferredScrollerStyle]; BOOL onlyShowDuringScrolling = (preferredScrollerStyle == NSScrollerStyleOverlay); BOOL usesCustomScrollers = [TPCPreferences themeChannelViewUsesCustomScrollers]; BOOL usingWebKit2 = self.backingView.isUsingWebKit2; return (onlyShowDuringScrolling == NO && usesCustomScrollers && usingWebKit2); } - (NSString *)initialDocument { NSMutableDictionary *templateTokens = [self generateOverrideStyle]; templateTokens[@"applicationResourcePath"] = [TPCPathInfo applicationResources]; templateTokens[@"applicationTemplatesPath"] = theme().applicationTemplateRepositoryPath; templateTokens[@"activeStyleAbsolutePath"] = self.baseURL.path; templateTokens[@"activeStyleCSSFiles"] = theme().temporaryCSSFilePaths; templateTokens[@"activeStyleJSFiles"] = theme().temporaryJSFilePaths; templateTokens[@"cacheToken"] = themeController().cacheToken; templateTokens[@"configuredServerName"] = self.associatedClient.networkNameAlt; templateTokens[@"isReloadingStyle"] = @(self.reloadingTheme); templateTokens[@"operatingSystemVersion"] = [XRSystemInformation systemStandardVersion]; TVCMainWindowAppearance *appearance = self.attachedWindow.userInterfaceObjects; templateTokens[@"appearanceDescription"] = appearance.shortAppearanceDescription; templateTokens[@"sidebarInversionIsEnabled"] = @(appearance.isDarkAppearance); templateTokens[@"userConfiguredTextEncoding"] = [NSString charsetRepFromStringEncoding:self.associatedClient.config.primaryEncoding]; templateTokens[@"userStyleSheetRules"] = [TPCPreferences themeUserStyleSheetRules]; templateTokens[@"usesCustomScrollers"] = @([self usesCustomScrollers]); IRCChannel *channel = self.associatedChannel; if (channel) { templateTokens[@"isChannelView"] = @(channel.isChannel); templateTokens[@"isPrivateMessageView"] = @(channel.isPrivateMessage); templateTokens[@"isUtilityView"] = @(channel.isUtility); templateTokens[@"channelName"] = channel.name; templateTokens[@"viewTypeToken"] = channel.channelTypeString; } else { templateTokens[@"viewTypeToken"] = @"server"; } if ([TPCPreferences rightToLeftFormatting]) { templateTokens[@"textDirectionToken"] = @"rtl"; } else { templateTokens[@"textDirectionToken"] = @"ltr"; } if ([themeSettings() underlyingWindowColorIsDark]) { templateTokens[@"appearanceToken"] = @"dark"; } else { templateTokens[@"appearanceToken"] = @"light"; } return [TVCLogRenderer renderTemplateNamed:@"baseLayout" attributes:templateTokens]; } - (NSMutableDictionary *)generateOverrideStyle { NSMutableDictionary *templateTokens = [NSMutableDictionary dictionary]; // ---- // NSFont *channelFont = themeSettings().themeChannelViewFont; if (channelFont == nil) { channelFont = [TPCPreferences themeChannelViewFont]; } templateTokens[@"userConfiguredFontName"] = channelFont.fontName; templateTokens[@"userConfiguredFontSize"] = @(channelFont.pointSize * (72.0 / 96.0)); // ---- // double indentOffset = themeSettings().indentationOffset; if (round(indentOffset) < 0.0 || [TPCPreferences rightToLeftFormatting]) { templateTokens[@"nicknameIndentationAvailable"] = @(NO); } else { templateTokens[@"nicknameIndentationAvailable"] = @(YES); NSString *timeFormat = themeSettings().themeTimestampFormat; if (timeFormat == nil) { timeFormat = [TPCPreferences themeTimestampFormat]; } NSString *time = TXFormattedTimestamp([NSDate date], timeFormat); NSSize textSize = [time sizeWithAttributes:@{NSFontAttributeName : channelFont}]; templateTokens[@"predefinedTimestampWidth"] = @(textSize.width + indentOffset); } // ---- // return templateTokens; } #pragma mark - #pragma mark LogView Delegate - (void)logViewWebViewFinishedLoading { if (self.loaded == NO) { self.loaded = YES; } else { return; } self.viewLoadedTimestamp = [NSDate timeIntervalSince1970]; IRCChannel *channel = self.associatedChannel; NSString *viewType = nil; if (channel) { viewType = channel.channelTypeString; } else { viewType = @"server"; } [self _evaluateFunction:@"Textual.viewInitiated" withArguments:@[ NSDictionaryNilValue(viewType), NSDictionaryNilValue(self.associatedClient.uniqueIdentifier), NSDictionaryNilValue(channel.uniqueIdentifier), NSDictionaryNilValue(channel.name) ]]; double textSizeMultiplier = self.attachedWindow.textSizeMultiplier; NSUInteger scrollbackLimit = [TPCPreferences scrollbackVisibleLimit]; [self _evaluateFunction:@"_Textual.viewFinishedLoading" withArguments: @[ @{ @"selected" : @(self.selected), @"visible" : @(self.visible), @"reloadingTheme" : @(self.reloadingTheme), // TODO: Fix this always being false @"textSizeMultiplier" : @(textSizeMultiplier), @"scrollbackLimit" : @(scrollbackLimit) } ]]; [self setInitialTopic]; [self reloadHistory]; [RZNotificationCenter() postNotificationName:TVCLogControllerViewFinishedLoadingNotification object:self]; [self.printingQueue updateReadinessState:self]; } - (void)logViewWebViewClosedUnexpectedly { [self clearBackingView]; } - (void)logViewWebViewKeyDown:(NSEvent *)e { [self.attachedWindow redirectKeyDown:e]; } - (void)logViewWebViewReceivedDropWithFile:(NSString *)filename { [menuController() memberSendDroppedFilesToSelectedChannel:@[filename]]; } @end #pragma mark - @implementation TVCLogControllerPrintOperationContext @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCLogLine.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "TXGlobalModels.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCUserNicknameColorStyleGeneratorPrivate.h" #import "TPCPreferencesLocal.h" #import "TPCThemeController.h" #import "TPCTheme.h" #import "TLOFileLoggerPrivate.h" #import "TVCLogLineInternal.h" #import "TVCLogLineXPCPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TVCLogLineUndefinedNicknameFormat = @"<%@%n>"; NSString * const TVCLogLineActionNicknameFormat = @"%@ "; NSString * const TVCLogLineNoticeNicknameFormat = @"-%@-"; NSString * const TVCLogLineSpecialNoticeMessageFormat = @"[%@]: %@"; NSString * const TVCLogLineDefaultCommandValue = @"-100"; @interface TVCLogLine () @property (readonly, copy) NSDictionary *dictionaryValue; @end @implementation TVCLogLine DESIGNATED_INITIALIZER_EXCEPTION_BODY_BEGIN - (instancetype)init { if ((self = [super init])) { [self populateDefaultsPostflight]; return self; } return nil; } DESIGNATED_INITIALIZER_EXCEPTION_BODY_END - (nullable instancetype)initWithData:(NSData *)data { NSParameterAssert(data != nil); if ((self = [super init])) { return [NSKeyedUnarchiver unarchiveObjectWithData:data]; } return nil; } + (TVCLogLine *)logLineFromXPCObject:(TVCLogLineXPC *)xpcObject { NSParameterAssert(xpcObject != nil); /* In earlier versions of the historic log database, the unique identifier was not stored in the archived data of the log line. We need a unique identifier now, which the database automatically creates if none is present, but it does it without unarchiving the data because the process does not have this class. It therefore just attaches the unique identifier to the XPC object. We can then write it out here. */ /* We check if the object's unique identifier is nil before setting the database's value because the value may have already been unarchived if it is present. */ TVCLogLine *object = [NSKeyedUnarchiver unarchiveObjectWithData:xpcObject.data]; if (object->_uniqueIdentifier == nil) { object->_uniqueIdentifier = [xpcObject.uniqueIdentifier copy]; } return [object copy]; } - (BOOL)populateWithDecoder:(NSCoder *)aDecoder { self->_receivedAt = [aDecoder decodeObjectOfClass:[NSDate class] forKey:@"receivedAt"]; self->_excludeKeywords = [aDecoder decodeObjectOfClasses:[NSSet setWithObjects:[NSArray class], [NSString class], nil] forKey:@"excludeKeywords"]; self->_highlightKeywords = [aDecoder decodeObjectOfClasses:[NSSet setWithObjects:[NSArray class], [NSString class], nil] forKey:@"highlightKeywords"]; self->_rendererAttributes = [aDecoder decodeDictionaryForKey:@"rendererAttributes"]; self->_isEncrypted = [aDecoder decodeBoolForKey:@"isEncrypted"]; self->_isFirstForDay = [aDecoder decodeBoolForKey:@"isFirstForDay"]; self->_command = [aDecoder decodeStringForKey:@"command"]; self->_messageBody = [aDecoder decodeStringForKey:@"messageBody"]; self->_nickname = [aDecoder decodeStringForKey:@"nickname"]; self->_lineType = [aDecoder decodeIntegerForKey:@"lineType"]; self->_memberType = [aDecoder decodeIntegerForKey:@"memberType"]; self->_uniqueIdentifier = [aDecoder decodeStringForKey:@"uniqueIdentifier"]; self->_sessionIdentifier = [aDecoder decodeIntegerForKey:@"sessionIdentifier"]; [self computeNicknameColorStyle]; return YES; } - (void)populateDefaultsPostflight { [self populateDefaultUniqueIdentifier]; [self populateDefaultSessionIdentifier]; SetVariableIfNil(self->_command, TVCLogLineDefaultCommandValue) SetVariableIfNil(self->_messageBody, @"") SetVariableIfNil(self->_receivedAt, [NSDate date]) if (self->_lineType == TVCLogLineTypeActionNoHighlight) { self->_lineType = TVCLogLineTypeAction; self->_highlightKeywords = nil; } else if (self->_lineType == TVCLogLineTypePrivateMessageNoHighlight) { self->_lineType = TVCLogLineTypePrivateMessage; self->_highlightKeywords = nil; } } - (void)populateDefaultUniqueIdentifier { if (self->_uniqueIdentifier != nil) { return; } self->_uniqueIdentifier = [self.class newUniqueIdentifier]; } - (void)populateDefaultSessionIdentifier { if (self->_sessionIdentifier != 0) { return; } self->_sessionIdentifier = [self.class currentSessionIdentifier]; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.command forKey:@"command"]; [aCoder encodeObject:self.messageBody forKey:@"messageBody"]; [aCoder maybeEncodeObject:self.excludeKeywords forKey:@"excludeKeywords"]; [aCoder maybeEncodeObject:self.highlightKeywords forKey:@"highlightKeywords"]; [aCoder maybeEncodeObject:self.rendererAttributes forKey:@"rendererAttributes"]; [aCoder maybeEncodeObject:self.nickname forKey:@"nickname"]; [aCoder encodeBool:self.isEncrypted forKey:@"isEncrypted"]; [aCoder encodeBool:self.isFirstForDay forKey:@"isFirstForDay"]; [aCoder encodeObject:self.receivedAt forKey:@"receivedAt"]; [aCoder encodeInteger:self.lineType forKey:@"lineType"]; [aCoder encodeInteger:self.memberType forKey:@"memberType"]; [aCoder encodeObject:self.uniqueIdentifier forKey:@"uniqueIdentifier"]; [aCoder encodeInteger:self.sessionIdentifier forKey:@"sessionIdentifier"]; } + (BOOL)supportsSecureCoding { return YES; } - (TVCLogLineXPC *)xpcObjectForTreeItem:(IRCTreeItem *)treeItem { NSParameterAssert(treeItem != nil); NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self]; TVCLogLineXPC *xpcObject = [[TVCLogLineXPC alloc] initWithLogLineData:data uniqueIdentifier:self.uniqueIdentifier viewIdentifier:treeItem.uniqueIdentifier sessionIdentifier:self.sessionIdentifier]; return xpcObject; } + (NSString *)newUniqueIdentifier { NSString *printIdentifier = [NSString stringWithUUID]; // Example: 68753A44-4D6F-1226-9C60-0050E4C00067 return [printIdentifier substringFromIndex:19]; // Example: 9C60-0050E4C00067 } + (NSUInteger)currentSessionIdentifier { static NSUInteger sessionIdentifier = 0; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sessionIdentifier = TXRandomNumber(999999); }); return sessionIdentifier; } - (BOOL)fromCurrentSession { return (self.sessionIdentifier == [self.class currentSessionIdentifier]); } + (nullable NSString *)stringForLineType:(TVCLogLineType)type { #define _dv(lineType, returnValue) case (lineType): { return (returnValue); break; } switch (type) { _dv(TVCLogLineTypeAction, @"action") _dv(TVCLogLineTypeActionNoHighlight, @"action") _dv(TVCLogLineTypeCTCP, @"ctcp") _dv(TVCLogLineTypeCTCPQuery, @"ctcp") _dv(TVCLogLineTypeCTCPReply, @"ctcp") _dv(TVCLogLineTypeDCCFileTransfer, @"dcc-file-transfer") _dv(TVCLogLineTypeDebug, @"debug") _dv(TVCLogLineTypeInvite, @"invite") _dv(TVCLogLineTypeJoin, @"join") _dv(TVCLogLineTypeKick, @"kick") _dv(TVCLogLineTypeKill, @"kill") _dv(TVCLogLineTypeMode, @"mode") _dv(TVCLogLineTypeNick, @"nick") _dv(TVCLogLineTypeNotice, @"notice") _dv(TVCLogLineTypeOffTheRecordEncryptionStatus, @"off-the-record-encryption-status") _dv(TVCLogLineTypePart, @"part") _dv(TVCLogLineTypePrivateMessage, @"privmsg") _dv(TVCLogLineTypePrivateMessageNoHighlight, @"privmsg") _dv(TVCLogLineTypeQuit, @"quit") _dv(TVCLogLineTypeTopic, @"topic") _dv(TVCLogLineTypeWebsite, @"website") default: { return nil; } } #undef _dv } + (NSString *)stringForMemberType:(TVCLogLineMemberType)type { if (type == TVCLogLineMemberTypeLocalUser) { return @"myself"; } else { return @"normal"; } } - (nullable NSString *)lineTypeString { return [self.class stringForLineType:self.lineType]; } - (NSString *)memberTypeString { return [self.class stringForMemberType:self.memberType]; } - (NSString *)formattedTimestamp { return [self formattedTimestampWithFormat:nil]; } - (NSString *)formattedTimestampWithFormat:(nullable NSString *)format { if (format.length == 0) { format = themeSettings().themeTimestampFormat; } if (format.length == 0) { format = [TPCPreferences themeTimestampFormat]; } if (format.length == 0) { format = [TPCPreferences themeTimestampFormatDefault]; } NSString *time = TXFormattedTimestamp(self.receivedAt, format); return time; } - (nullable NSString *)formattedNicknameInChannel:(nullable IRCChannel *)channel { return [self formattedNicknameInChannel:channel withFormat:nil]; } - (nullable NSString *)formattedNicknameInChannel:(nullable IRCChannel *)channel withFormat:(nullable NSString *)format { if (self.nickname == nil) { return nil; } if (format == nil) { if (self.lineType == TVCLogLineTypeAction) { return [NSString stringWithFormat:TVCLogLineActionNicknameFormat, self.nickname]; } else if (self.lineType == TVCLogLineTypeNotice) { return [NSString stringWithFormat:TVCLogLineNoticeNicknameFormat, self.nickname]; } } return [channel.associatedClient formatNickname:self.nickname inChannel:channel withFormat:format]; } - (NSString *)renderedBodyForTranscriptLog { return [self renderedBodyForTranscriptLogInChannel:nil]; } - (NSString *)renderedBodyForTranscriptLogInChannel:(nullable IRCChannel *)channel { NSMutableString *s = [NSMutableString string]; NSString *timeFormatted = [self formattedTimestampWithFormat:TLOFileLoggerISOStandardClockFormat]; if (timeFormatted) { [s appendString:timeFormatted]; [s appendString:@" "]; } NSString *nicknameFormatted = nil; if (self.lineType == TVCLogLineTypeAction) { nicknameFormatted = [self formattedNicknameInChannel:channel withFormat:TLOFileLoggerActionNicknameFormat]; } else if (self.lineType == TVCLogLineTypeNotice) { nicknameFormatted = [self formattedNicknameInChannel:channel withFormat:TLOFileLoggerNoticeNicknameFormat]; } else { nicknameFormatted = [self formattedNicknameInChannel:channel withFormat:TLOFileLoggerUndefinedNicknameFormat]; } if (nicknameFormatted) { [s appendString:nicknameFormatted]; [s appendString:@" "]; } [s appendString:self.messageBody]; return s.stripIRCEffects; } - (void)computeNicknameColorStyle { if (self.nickname != nil && (self.lineType == TVCLogLineTypePrivateMessage || self.lineType == TVCLogLineTypePrivateMessageNoHighlight || self.lineType == TVCLogLineTypeAction || self.lineType == TVCLogLineTypeActionNoHighlight)) { BOOL isOverride = NO; self->_nicknameColorStyle = [IRCUserNicknameColorStyleGenerator nicknameColorStyleForString:self.nickname isOverride:&isOverride]; self->_nicknameColorStyleOverride = isOverride; } else { self->_nicknameColorStyle = nil; } } - (void)populateDuringCopy:(__kindof XRPortablePropertyObject *)newObject mutableCopy:(BOOL)mutableCopy { TVCLogLine *object = (TVCLogLine *)newObject; object->_uniqueIdentifier = self->_uniqueIdentifier; object->_isEncrypted = self->_isEncrypted; object->_isFirstForDay = self->_isFirstForDay; object->_excludeKeywords = self->_excludeKeywords; object->_highlightKeywords = self->_highlightKeywords; object->_rendererAttributes = self->_rendererAttributes; object->_receivedAt = self->_receivedAt; object->_command = self->_command; object->_messageBody = self->_messageBody; object->_nickname = self->_nickname; object->_nicknameColorStyle = self->_nicknameColorStyle; object->_nicknameColorStyleOverride = self->_nicknameColorStyleOverride; object->_lineType = self->_lineType; object->_memberType = self->_memberType; object->_sessionIdentifier = self->_sessionIdentifier; } - (__kindof XRPortablePropertyObject *)mutableClass { return [TVCLogLineMutable self]; } @end #pragma mark - @implementation TVCLogLineMutable @dynamic command; @dynamic excludeKeywords; @dynamic highlightKeywords; @dynamic rendererAttributes; @dynamic isEncrypted; @dynamic isFirstForDay; @dynamic lineType; @dynamic memberType; @dynamic messageBody; @dynamic nickname; @dynamic receivedAt; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [TVCLogLine self]; } - (void)setIsEncrypted:(BOOL)isEncrypted { if (self->_isEncrypted != isEncrypted) { self->_isEncrypted = isEncrypted; } } - (void)setIsFirstForDay:(BOOL)isFirstForDay { if (self->_isFirstForDay != isFirstForDay) { self->_isFirstForDay = isFirstForDay; } } - (void)setExcludeKeywords:(nullable NSArray *)excludeKeywords { if (self->_excludeKeywords != excludeKeywords) { self->_excludeKeywords = [excludeKeywords copy]; } } - (void)setHighlightKeywords:(nullable NSArray *)highlightKeywords { if (self->_highlightKeywords != highlightKeywords) { self->_highlightKeywords = [highlightKeywords copy]; } } - (void)setRendererAttributes:(nullable NSDictionary *)rendererAttributes { if (self->_rendererAttributes != rendererAttributes) { self->_rendererAttributes = [rendererAttributes copy]; } } - (void)setReceivedAt:(NSDate *)receivedAt { NSParameterAssert(receivedAt != nil); if (self->_receivedAt != receivedAt) { self->_receivedAt = [receivedAt copy]; } } - (void)setCommand:(NSString *)command { NSParameterAssert(command != nil); if (self->_command != command) { self->_command = [command copy]; } } - (void)setMessageBody:(NSString *)messageBody { NSParameterAssert(messageBody != nil); if (self->_messageBody != messageBody) { self->_messageBody = [messageBody copy]; } } - (void)setNickname:(nullable NSString *)nickname { if (self->_nickname != nickname) { self->_nickname = [nickname copy]; [self computeNicknameColorStyle]; } } - (void)setMemberType:(TVCLogLineMemberType)memberType { if (self->_memberType != memberType) { self->_memberType = memberType; } } - (void)setLineType:(TVCLogLineType)lineType { if (self->_lineType != lineType) { self->_lineType = lineType; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCLogPolicy.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" #import "TXMenuControllerPrivate.h" #import "IRCChannel.h" #import "TDCAlert.h" #import "TLOLocalization.h" #import "TLOpenLink.h" #import "TPCPreferencesLocal.h" #import "TPCThemeController.h" #import "TVCLogController.h" #import "TVCLogViewPrivate.h" #import "TVCLogPolicyPrivate.h" NS_ASSUME_NONNULL_BEGIN /* Specific menu items are gathered and inserted at specific locations */ #define _WebKit1MenuItemTagInspectElement 2024 #define _WebKit1MenuItemTagLookupInDictionary WebMenuItemTagLookUpInDictionary #define _WebKit1MenuItemTagSearchWithGoogle WebMenuItemTagSearchWeb #define _WebKit2MenuItemTagInspectElement 57 #define _WebKit2MenuItemTagLookupInDictionary 22 #define _WebKit2MenuItemTagSearchWithGoogle 21 @implementation TVCLogPolicy - (NSArray *)constructContextMenuItemsForWebView:(TVCLogView *)webView defaultMenuItems:(NSArray *)defaultMenuItems { TVCLogController *viewController = webView.viewController; BOOL isWebKit2 = webView.isUsingWebKit2; NSMutableArray *menuItems = [NSMutableArray array]; if (self.anchorURL) { NSMenu *urlMenu = menuController().channelViewURLMenu; for (NSMenuItem *item in urlMenu.itemArray) { NSMenuItem *newItem = [item copy]; [newItem setUserInfo:self.anchorURL recursively:YES]; [menuItems addObject:newItem]; } self.anchorURL = nil; } else if (self.nickname) { if (viewController.associatedChannel == nil || viewController.associatedChannel.isUtility) { NSMenuItem *noActionMenuItem = [[NSMenuItem alloc] initWithTitle:TXTLS(@"BasicLanguage[7kc-mo]") action:nil keyEquivalent:@""]; [menuItems addObject:noActionMenuItem]; } else { NSMenu *memberMenu = menuController().userControlMenu; for (NSMenuItem *item in memberMenu.itemArray) { NSMenuItem *newItem = [item copy]; [newItem setUserInfo:self.nickname recursively:YES]; [menuItems addObject:newItem]; } } self.nickname = nil; } else if (self.channelName) { NSMenu *chanMenu = menuController().channelViewChannelNameMenu; for (NSMenuItem *item in chanMenu.itemArray) { NSMenuItem *newItem = [item copy]; [newItem setUserInfo:self.channelName recursively:YES]; [menuItems addObject:newItem]; } self.channelName = nil; } else { NSMenu *menu = menuController().channelViewGeneralMenu; NSMenuItem *inspectElementItem = nil; NSMenuItem *lookupInDictionaryItem = nil; NSMenuItem *searchWithGoogleItem = nil; TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN for (NSMenuItem *item in defaultMenuItems) { if ((item.tag == _WebKit1MenuItemTagLookupInDictionary && isWebKit2 == NO) || (item.tag == _WebKit2MenuItemTagLookupInDictionary && isWebKit2)) { lookupInDictionaryItem = [item copy]; } else if ((item.tag == _WebKit1MenuItemTagInspectElement && isWebKit2 == NO) || (item.tag == _WebKit2MenuItemTagInspectElement && isWebKit2)) { inspectElementItem = [item copy]; } else if ((item.tag == _WebKit1MenuItemTagSearchWithGoogle && isWebKit2 == NO) || (item.tag == _WebKit2MenuItemTagSearchWithGoogle && isWebKit2)) { searchWithGoogleItem = [item copy]; } } TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END for (NSMenuItem *item in menu.itemArray) { NSMenuItem *newItem = [item copy]; if (newItem.tag == MTWKGeneralSearchWithGoogle) { if (searchWithGoogleItem != nil) { [menuItems addObject:searchWithGoogleItem]; continue; } } else if (newItem.tag == MTWKGeneralLookUpInDictionary) { if (lookupInDictionaryItem != nil) { [menuItems addObject:lookupInDictionaryItem]; continue; } } [menuItems addObject:newItem]; } if ([TPCPreferences developerModeEnabled]) { [menuItems addObject:[NSMenuItem separatorItem]]; [menuItems addObject: [NSMenuItem menuItemWithTitle:TXTLS(@"BasicLanguage[6cw-ni]") target:menuController() action:@selector(copyLogAsHtml:)]]; [menuItems addObject: [NSMenuItem menuItemWithTitle:TXTLS(@"BasicLanguage[ngd-ms]") target:menuController() action:@selector(forceReloadTheme:)]]; if (inspectElementItem == nil) { if (isWebKit2) { [menuItems addObject: [NSMenuItem menuItemWithTitle:TXTLS(@"BasicLanguage[tfj-m9]") target:menuController() action:@selector(openWebInspector:)]]; } } else { [menuItems addObject:inspectElementItem]; } } } return [menuItems copy]; } - (NSMenu *)constructContextMenuForWebView:(TVCLogView *)webView withDefaultMenuItems:(NSArray *)defaultMenuItems { NSMenu *contextMenu = [[NSMenu alloc] initWithTitle:TXLocalizationNotNeeded(@"Context Menu")]; NSArray *menuItems = [self constructContextMenuItemsForWebView:webView defaultMenuItems:defaultMenuItems]; for (NSMenuItem *menuItem in menuItems) { [contextMenu addItem:menuItem]; } return contextMenu; } - (void)displayContextMenuInWebView:(TVCLogView *)webView { if (webView.isUsingWebKit2 == NO) { return; } NSMenu *contextMenu = [self constructContextMenuForWebView:webView withDefaultMenuItems:@[]]; NSView *webViewBacking = webView.webView; NSWindow *webViewWindow = webViewBacking.window; NSPoint mouseLocationGlobal = [NSEvent mouseLocation]; NSRect mouseLocationLocal = [webViewWindow convertRectFromScreen:NSMakeRect(mouseLocationGlobal.x, mouseLocationGlobal.y, 0, 0)]; NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeRightMouseUp location:mouseLocationLocal.origin modifierFlags:0 timestamp:0 windowNumber:webViewWindow.windowNumber context:nil eventNumber:0 clickCount:0 pressure:0]; [NSMenu popUpContextMenu:contextMenu withEvent:event forView:webViewBacking]; } #pragma mark - #pragma mark WebKit Delegate TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN - (NSArray *)webView1:(WebView *)webView logView:(TVCLogView *)logView contextMenuWithDefaultMenuItems:(NSArray *)defaultMenuItems { return [self constructContextMenuItemsForWebView:logView defaultMenuItems:defaultMenuItems]; } - (void)webView1:(WebView *)webView logView:(TVCLogView *)logView resource:(id)identifier didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge fromDataSource:(WebDataSource *)dataSource { [challenge.sender cancelAuthenticationChallenge:challenge]; } - (NSUInteger)webView1:(WebView *)webView logView:(TVCLogView *)logView dragDestinationActionMaskForDraggingInfo:(id)draggingInfo { NSPasteboard *pboard = [draggingInfo draggingPasteboard]; if ([pboard.types containsObject:NSFilenamesPboardType]) { return WebDragDestinationActionAny; } return WebDragDestinationActionNone; } - (void)webView1:(WebView *)webView logView:(TVCLogView *)logView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { NSInteger action = [actionInformation integerForKey:WebActionNavigationTypeKey]; if (action == WebNavigationTypeLinkClicked) { [listener ignore]; NSURL *actionURL = actionInformation[WebActionOriginalURLKey]; [self openWebpage:actionURL]; } else { [listener use]; } } TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END #pragma mark - #pragma mark WebKit2 Delegate - (void)webView2:(WKWebView *)webView logView:(TVCLogView *)logView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nonnull))completionHandler { NSString *authenticationMethod = challenge.protectionSpace.authenticationMethod; if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } else { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } } - (void)webView2:(WKWebView *)webView logView:(TVCLogView *)logView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { WKNavigationType action = navigationAction.navigationType; if (action == WKNavigationTypeLinkActivated) { NSURL *actionURL = navigationAction.request.URL; decisionHandler(WKNavigationActionPolicyCancel); [self openWebpage:actionURL]; } else { decisionHandler(WKNavigationActionPolicyAllow); } } - (NSMenu *)webView2:(WKWebView *)webView logView:(TVCLogView *)logView contextMenuWithDefaultMenu:(NSMenu *)defaultMenu { return [self constructContextMenuForWebView:logView withDefaultMenuItems:defaultMenu.itemArray]; } #pragma mark - #pragma mark Shared - (void)channelNameDoubleClicked { [menuController() joinChannelClicked:self.channelName]; self.channelName = nil; } - (void)nicknameDoubleClicked { menuController().pointedNickname = self.nickname; [menuController() memberInChannelViewDoubleClicked:nil]; self.nickname = nil; } - (void)topicBarDoubleClicked { [menuController() showChannelModifyTopicSheet:nil]; } - (void)openWebpage:(NSURL *)webpageURL { NSParameterAssert(webpageURL != nil); BOOL openInBackground = [TPCPreferences openBrowserInBackground]; NSUInteger keyboardKeys = ([NSEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask); if ((keyboardKeys & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) { openInBackground = !openInBackground; } if ([webpageURL.scheme isEqualToString:@"http"] || [webpageURL.scheme isEqualToString:@"https"] || [webpageURL.scheme isEqualToString:@"textual"]) { [TLOpenLink open:webpageURL inBackground:openInBackground]; return; } NSString *applicationName = [RZWorkspace() nameOfApplicationToOpenURL:webpageURL]; BOOL openLink = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[5oq-vv]", webpageURL.absoluteString) title:TXTLS(@"Prompts[2ul-cl]", applicationName) defaultButton:TXTLS(@"Prompts[mvh-ms]") alternateButton:TXTLS(@"Prompts[99q-gg]") suppressionKey:@"open_non_http_url_warning" suppressionText:nil]; if (openLink == NO) { return; } [TLOpenLink open:webpageURL inBackground:openInBackground]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCLogRenderer.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "GTMEncodeHTML.h" #import "NSColorHelper.h" #import "NSStringHelper.h" #import "IRCClientConfig.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCChannelUser.h" #import "IRCColorFormat.h" #import "IRCUser.h" #import "IRCUserNicknameColorStyleGeneratorPrivate.h" #import "TPCPreferencesLocal.h" #import "TPCThemeController.h" #import "TPCTheme.h" #import "THOPluginDispatcherPrivate.h" #import "THOUnicodeHelper.h" #import "TLOLinkParser.h" #import "TVCLogController.h" #import "TVCLogLine.h" #import "TVCLogRenderer.h" NS_ASSUME_NONNULL_BEGIN @interface TVCLogRenderer () @property (nonatomic, copy, nullable) NSString *body; @property (nonatomic, copy, nullable) id finalResult; @property (nonatomic, strong, nullable) NSMutableAttributedString *bodyWithAttributes; @property (nonatomic, strong, nullable) NSMutableDictionary *renderedBodyOpenAttributes; @property (nonatomic, copy) NSDictionary *rendererAttributes; @property (nonatomic, strong) NSMutableDictionary *outputDictionary; @property (nonatomic, weak) TVCLogController *viewController; @property (nonatomic, assign) TVCLogLineType lineType; @property (nonatomic, assign) TVCLogLineMemberType memberType; @property (nonatomic, assign) BOOL escapeBody; @end NSString * const TVCLogRendererFormattingForegroundColorAttribute = @"TVCLogRendererFormattingForegroundColorAttribute"; NSString * const TVCLogRendererFormattingBackgroundColorAttribute = @"TVCLogRendererFormattingBackgroundColorAttribute"; NSString * const TVCLogRendererFormattingBoldTextAttribute = @"TVCLogRendererFormattingBoldTextAttribute"; NSString * const TVCLogRendererFormattingItalicTextAttribute = @"TVCLogRendererFormattingItalicTextAttribute"; NSString * const TVCLogRendererFormattingMonospaceTextAttribute = @"TVCLogRendererFormattingMonospaceTextAttribute"; NSString * const TVCLogRendererFormattingStrikethroughTextAttribute = @"TVCLogRendererFormattingStrikethroughTextAttribute"; NSString * const TVCLogRendererFormattingUnderlineTextAttribute = @"TVCLogRendererFormattingUnderlineTextAttribute"; NSString * const TVCLogRendererFormattingChannelNameAttribute = @"TVCLogRendererFormattingChannelNameAttribute"; NSString * const TVCLogRendererFormattingConversationTrackingAttribute = @"TVCLogRendererFormattingConversationTrackingAttribute"; NSString * const TVCLogRendererFormattingKeywordHighlightAttribute = @"TVCLogRendererFormattingKeywordHighlightAttribute"; NSString * const TVCLogRendererFormattingURLAttribute = @"TVCLogRendererFormattingURLAttribute"; NSString * const TVCLogRendererConfigurationRenderLinksAttribute = @"TVCLogRendererConfigurationRenderLinksAttribute"; NSString * const TVCLogRendererConfigurationLineTypeAttribute = @"TVCLogRendererConfigurationLineTypeAttribute"; NSString * const TVCLogRendererConfigurationMemberTypeAttribute = @"TVCLogRendererConfigurationMemberTypeAttribute"; NSString * const TVCLogRendererConfigurationHighlightKeywordsAttribute = @"TVCLogRendererConfigurationHighlightKeywordsAttribute"; NSString * const TVCLogRendererConfigurationExcludedKeywordsAttribute = @"TVCLogRendererConfigurationExcludedKeywordsAttribute"; NSString * const TVCLogRendererConfigurationDoNotEscapeBodyAttribute = @"TVCLogRendererConfigurationDoNotEscapeBodyAttribute"; NSString * const TVCLogRendererConfigurationAttributedStringPreferredFontAttribute = @"TVCLogRendererConfigurationAttributedStringPreferredFontAttribute"; NSString * const TVCLogRendererConfigurationAttributedStringPreferredFontColorAttribute = @"TVCLogRendererConfigurationAttributedStringPreferredFontColorAttribute"; NSString * const TVCLogRendererResultsListOfLinksInBodyAttribute = @"TVCLogRendererResultsListOfLinksInBodyAttribute"; NSString * const TVCLogRendererResultsListOfLinksMappedInBodyAttribute = @"TVCLogRendererResultsListOfLinksMappedInBodyAttribute"; NSString * const TVCLogRendererResultsKeywordMatchFoundAttribute = @"TVCLogRendererResultsKeywordMatchFoundAttribute"; NSString * const TVCLogRendererResultsListOfUsersFoundAttribute = @"TVCLogRendererResultsListOfUsersFoundAttribute"; NSString * const TVCLogRendererResultsOriginalBodyWithoutEffectsAttribute = @"TVCLogRendererResultsOriginalBodyWithoutEffectsAttribute"; @implementation TVCLogRenderer - (instancetype)init { if ((self = [super init])) { self->_outputDictionary = [NSMutableDictionary dictionary]; } return self; } - (void)buildEffectsDictionary { NSString *body = self->_body; NSUInteger bodyLength = body.length; UniChar charactersIn[bodyLength]; [body getCharacters:charactersIn range:body.range]; NSMutableAttributedString *bodyWithAttributes = [[NSMutableAttributedString alloc] initWithString:body attributes:nil]; [bodyWithAttributes beginEditing]; NSUInteger characterOffset = 0; for (NSUInteger i = 0; i < bodyLength; i++) { UniChar character = charactersIn[i]; NSUInteger characterPosition = (i - characterOffset); if (character < 0x20) { switch (character) { case IRCTextFormatterEffectBoldCharacter: { if (characterPosition > 0 && [bodyWithAttributes isAttributeSet:TVCLogRendererFormattingBoldTextAttribute atIndex:characterPosition]) { [bodyWithAttributes removeAttribute:TVCLogRendererFormattingBoldTextAttribute startingAt:characterPosition]; } else { [bodyWithAttributes addAttribute:TVCLogRendererFormattingBoldTextAttribute value:@(YES) startingAt:characterPosition]; } [bodyWithAttributes deleteCharactersInRange:NSMakeRange(characterPosition, 1)]; characterOffset++; continue; } case IRCTextFormatterEffectColorAsDigitCharacter: case IRCTextFormatterEffectColorAsHexCharacter: { id foregroundColor = nil; id backgroundColor = nil; NSUInteger colorOffset = [bodyWithAttributes.string colorComponentsOfCharacter:character startingAt:characterPosition foregroundColor:&foregroundColor backgroundColor:&backgroundColor]; if (foregroundColor != nil) { [bodyWithAttributes addAttribute:TVCLogRendererFormattingForegroundColorAttribute value:foregroundColor startingAt:characterPosition]; } else if (characterPosition > 0 && [bodyWithAttributes isAttributeSet:TVCLogRendererFormattingForegroundColorAttribute atIndex:characterPosition]) { [bodyWithAttributes removeAttribute:TVCLogRendererFormattingForegroundColorAttribute startingAt:characterPosition]; } if (backgroundColor != nil) { [bodyWithAttributes addAttribute:TVCLogRendererFormattingBackgroundColorAttribute value:backgroundColor startingAt:characterPosition]; } else if (characterPosition > 0 && [bodyWithAttributes isAttributeSet:TVCLogRendererFormattingBackgroundColorAttribute atIndex:characterPosition]) { /* We only strip the background color if there is no longer a foreground color. A end character. */ if (foregroundColor == nil) { [bodyWithAttributes removeAttribute:TVCLogRendererFormattingBackgroundColorAttribute startingAt:characterPosition]; } } i += (colorOffset - 1); // For loop will increase this by one so we minus by one [bodyWithAttributes deleteCharactersInRange:NSMakeRange(characterPosition, colorOffset)]; characterOffset += colorOffset; continue; } case IRCTextFormatterTerminatingCharacter: { [bodyWithAttributes resetAttributesStaringAt:characterPosition]; [bodyWithAttributes deleteCharactersInRange:NSMakeRange(characterPosition, 1)]; characterOffset++; continue; } case IRCTextFormatterEffectItalicCharacter: case IRCTextFormatterEffectItalicCharacterOld: { if (characterPosition > 0 && [bodyWithAttributes isAttributeSet:TVCLogRendererFormattingItalicTextAttribute atIndex:characterPosition]) { [bodyWithAttributes removeAttribute:TVCLogRendererFormattingItalicTextAttribute startingAt:characterPosition]; } else { [bodyWithAttributes addAttribute:TVCLogRendererFormattingItalicTextAttribute value:@(YES) startingAt:characterPosition]; } [bodyWithAttributes deleteCharactersInRange:NSMakeRange(characterPosition, 1)]; characterOffset++; continue; } case IRCTextFormatterEffectMonospaceCharacter: { if (characterPosition > 0 && [bodyWithAttributes isAttributeSet:TVCLogRendererFormattingMonospaceTextAttribute atIndex:characterPosition]) { [bodyWithAttributes removeAttribute:TVCLogRendererFormattingMonospaceTextAttribute startingAt:characterPosition]; } else { [bodyWithAttributes addAttribute:TVCLogRendererFormattingMonospaceTextAttribute value:@(YES) startingAt:characterPosition]; } [bodyWithAttributes deleteCharactersInRange:NSMakeRange(characterPosition, 1)]; characterOffset++; continue; } case IRCTextFormatterEffectStrikethroughCharacter: { if (characterPosition > 0 && [bodyWithAttributes isAttributeSet:TVCLogRendererFormattingStrikethroughTextAttribute atIndex:characterPosition]) { [bodyWithAttributes removeAttribute:TVCLogRendererFormattingStrikethroughTextAttribute startingAt:characterPosition]; } else { [bodyWithAttributes addAttribute:TVCLogRendererFormattingStrikethroughTextAttribute value:@(YES) startingAt:characterPosition]; } [bodyWithAttributes deleteCharactersInRange:NSMakeRange(characterPosition, 1)]; characterOffset++; continue; } case IRCTextFormatterEffectUnderlineCharacter: { if (characterPosition > 0 && [bodyWithAttributes isAttributeSet:TVCLogRendererFormattingUnderlineTextAttribute atIndex:characterPosition]) { [bodyWithAttributes removeAttribute:TVCLogRendererFormattingUnderlineTextAttribute startingAt:characterPosition]; } else { [bodyWithAttributes addAttribute:TVCLogRendererFormattingUnderlineTextAttribute value:@(YES) startingAt:characterPosition]; } [bodyWithAttributes deleteCharactersInRange:NSMakeRange(characterPosition, 1)]; characterOffset++; continue; } // case } // switch } // character < 0x20 } // for loop [bodyWithAttributes endEditing]; NSString *stringWithoutEffects = bodyWithAttributes.string; self->_outputDictionary[TVCLogRendererResultsOriginalBodyWithoutEffectsAttribute] = stringWithoutEffects; self->_body = stringWithoutEffects; self->_bodyWithAttributes = bodyWithAttributes; } - (BOOL)isRenderingPRIVMSG { return (self->_lineType == TVCLogLineTypePrivateMessage || self->_lineType == TVCLogLineTypeAction); } - (BOOL)isRenderingPRIVMSG_or_NOTICE { return (self->_lineType == TVCLogLineTypePrivateMessage || self->_lineType == TVCLogLineTypeAction || self->_lineType == TVCLogLineTypeNotice); } - (BOOL)scanForKeywords { return ([self isRenderingPRIVMSG] && self->_memberType == TVCLogLineMemberTypeNormal); } - (void)stripDangerousUnicodeCharactersFromBody { if ([TPCPreferences automaticallyFilterUnicodeTextSpam] == NO) { return; } if (self->_lineType != TVCLogLineTypeAction && self->_lineType != TVCLogLineTypeCTCP && self->_lineType != TVCLogLineTypeCTCPQuery && self->_lineType != TVCLogLineTypeCTCPReply && self->_lineType != TVCLogLineTypeDCCFileTransfer && self->_lineType != TVCLogLineTypeNotice && self->_lineType != TVCLogLineTypePrivateMessage && self->_lineType != TVCLogLineTypeTopic) { return; } self->_body = [XRRegularExpression string:self->_body replacedByRegex:@"([\\p{InCombining_Diacritical_Marks}]{3,})" withString:CS_UnicodeReplacementCharacter]; } - (void)buildListOfLinksInBody { BOOL renderLinks = [self->_rendererAttributes boolForKey:TVCLogRendererConfigurationRenderLinksAttribute]; if (renderLinks == NO) { return; } NSMutableDictionary *linksMapped = [NSMutableDictionary dictionary]; NSArray *links = [TLOLinkParser locateLinksInString:self->_body]; for (AHHyperlinkScannerResult *link in links) { NSRange linkRange = link.range; NSString *linkString = link.stringValue; [self->_bodyWithAttributes addAttribute:TVCLogRendererFormattingURLAttribute value:link range:linkRange]; if (linksMapped[linkString] == nil) { linksMapped[linkString] = link.uniqueIdentifier; } } self->_outputDictionary[TVCLogRendererResultsListOfLinksInBodyAttribute] = links; self->_outputDictionary[TVCLogRendererResultsListOfLinksMappedInBodyAttribute] = [linksMapped copy]; } - (void)matchKeywords { if ([self scanForKeywords] == NO) { return; } id excludedKeywords = [self->_rendererAttributes arrayForKey:TVCLogRendererConfigurationExcludedKeywordsAttribute]; id highlightKeywords = [self->_rendererAttributes arrayForKey:TVCLogRendererConfigurationHighlightKeywordsAttribute]; if ([highlightKeywords count] == 0) { self->_outputDictionary[TVCLogRendererResultsKeywordMatchFoundAttribute] = @(NO); return; } NSMutableArray *excludeRanges = [NSMutableArray array]; for (NSString *excludeKeyword in excludedKeywords) { [self->_body enumerateMatchesOfString:excludeKeyword withBlock:^(NSRange range, BOOL *stop) { [excludeRanges addObject:[NSValue valueWithRange:range]]; } options:NSCaseInsensitiveSearch]; } BOOL foundKeyword = NO; switch ([TPCPreferences highlightMatchingMethod]) { case TXNicknameHighlightMatchTypeExact: case TXNicknameHighlightMatchTypePartial: { foundKeyword = [self matchKeywordsUsingNormalMatching:highlightKeywords excludedRanges:excludeRanges]; break; } case TXNicknameHighlightMatchTypeRegularExpression: { foundKeyword = [self matchKeywordsUsingRegularExpression:highlightKeywords excludedRanges:excludeRanges]; break; } } self->_outputDictionary[TVCLogRendererResultsKeywordMatchFoundAttribute] = @(foundKeyword); } - (BOOL)matchKeywordsUsingNormalMatching:(NSArray *)keywords excludedRanges:(NSArray *)excludedRanges { NSParameterAssert(keywords != nil); NSParameterAssert(excludedRanges != nil); NSString *body = self->_body; __block BOOL foundKeyword = NO; for (NSString *keyword in keywords) { [body enumerateMatchesOfString:keyword withBlock:^(NSRange range, BOOL *stop) { for (NSValue *excludedRange in excludedRanges) { if (NSIntersectionRange(range, excludedRange.rangeValue).length > 0) { return; } } if ([TPCPreferences highlightMatchingMethod] == TXNicknameHighlightMatchTypeExact) { if ([self sectionOfBodyIsSurroundedByNonAlphabeticals:range] == NO) { return; } } if ([self->_bodyWithAttributes isAttributeSet:TVCLogRendererFormattingURLAttribute inRange:range] == NO) { [self->_bodyWithAttributes addAttribute:TVCLogRendererFormattingKeywordHighlightAttribute value:@(YES) range:range]; foundKeyword = YES; *stop = YES; } } options:NSCaseInsensitiveSearch]; if (foundKeyword) { break; } } return foundKeyword; } - (BOOL)matchKeywordsUsingRegularExpression:(NSArray *)keywords excludedRanges:(NSArray *)excludedRanges { NSParameterAssert(keywords != nil); NSParameterAssert(excludedRanges != nil); NSString *body = self->_body; BOOL foundKeyword = NO; for (NSString *keyword in keywords) { NSRange range = [XRRegularExpression string:body rangeOfRegex:keyword withoutCase:YES]; if (range.location == NSNotFound) { continue; } BOOL enabled = YES; for (NSValue *excludedRange in excludedRanges) { if (NSIntersectionRange(range, excludedRange.rangeValue).length > 0) { enabled = NO; break; } } if (enabled == NO) { continue; } if ([self->_bodyWithAttributes isAttributeSet:TVCLogRendererFormattingURLAttribute inRange:range] == NO) { [self->_bodyWithAttributes addAttribute:TVCLogRendererFormattingKeywordHighlightAttribute value:@(YES) range:range]; foundKeyword = YES; break; } } return foundKeyword; } - (void)findAllChannelNames { if ([self isRenderingPRIVMSG_or_NOTICE] == NO) { return; } NSString *body = self->_body; [body enumerateMatchesOfString:@"#([a-zA-Z0-9\\#\\-]+)" withBlock:^(NSRange range, BOOL *stop) { if ([self sectionOfBodyIsSurroundedByNonAlphabeticals:range] == NO) { return; } if ([self->_bodyWithAttributes isAttributeSet:TVCLogRendererFormattingURLAttribute inRange:range] == NO) { [self->_bodyWithAttributes addAttribute:TVCLogRendererFormattingChannelNameAttribute value:@(YES) range:range]; } } options:(NSCaseInsensitiveSearch | NSRegularExpressionSearch)]; } - (BOOL)sectionOfBodyIsSurroundedByNonAlphabeticals:(NSRange)range { NSString *body = self->_body; NSUInteger bodyLength = body.length; UniChar aa = [body characterAtIndex:range.location]; if (CS_StringIsBase10Numeric(aa) || [THOUnicodeHelper isAlphabeticalCodePoint:aa]) { NSInteger leftLocation = (range.location - 1); if (leftLocation >= 0 && leftLocation < bodyLength) { UniChar bb = [body characterAtIndex:leftLocation]; if (CS_StringIsBase10Numeric(bb) || [THOUnicodeHelper isAlphabeticalCodePoint:bb]) { return NO; } } } UniChar cc = [body characterAtIndex:(NSMaxRange(range) - 1)]; if (CS_StringIsBase10Numeric(cc) || [THOUnicodeHelper isAlphabeticalCodePoint:cc]) { NSInteger rightLocation = NSMaxRange(range); if (rightLocation < bodyLength) { UniChar dd = [body characterAtIndex:rightLocation]; if (CS_StringIsBase10Numeric(dd) || [THOUnicodeHelper isAlphabeticalCodePoint:dd]) { return NO; } } } return YES; } - (void)scanBodyForChannelMembers { if ([self isRenderingPRIVMSG] == NO) { return; } NSString *body = self->_body; NSUInteger bodyLength = body.length; if (bodyLength == 0) { self->_outputDictionary[TVCLogRendererResultsListOfUsersFoundAttribute] = [NSSet set]; return; } IRCChannel *channel = self->_viewController.associatedChannel; NSArray *users = channel.memberList; __block NSUInteger totalNicknameCount = 0; __block NSUInteger totalNicknameLength = 0; NSMutableSet *userSet = [NSMutableSet set]; for (IRCChannelUser *user in users) { [body enumerateMatchesOfString:user.user.nickname withBlock:^(NSRange range, BOOL *stop) { if ([self sectionOfBodyIsSurroundedByNonAlphabeticals:range] == NO) { return; } if ([self->_bodyWithAttributes isAttributeSet:TVCLogRendererFormattingURLAttribute inRange:range] == NO) { [self->_bodyWithAttributes addAttribute:TVCLogRendererFormattingConversationTrackingAttribute value:@(YES) range:range]; if ([userSet containsObject:user] == NO) { [userSet addObject:user]; } if ([self->_bodyWithAttributes isAttributeSet:TVCLogRendererFormattingKeywordHighlightAttribute inRange:range] == NO) { totalNicknameCount += 1; totalNicknameLength += range.length; } } } options:NSCaseInsensitiveSearch]; } /* Calculate how much of the message is just nicknames. This is used when trying to stop highlight spam. Textual counts anything above 75% spam. */ if ([TPCPreferences automaticallyDetectHighlightSpam]) { double nicknamePercent = (((double)totalNicknameLength / bodyLength) * 100.0); if ((nicknamePercent > 75.0 && totalNicknameCount > 10) || (nicknamePercent > 50.0 && totalNicknameCount > 20)) { self->_outputDictionary[TVCLogRendererResultsKeywordMatchFoundAttribute] = @(NO); } } self->_outputDictionary[TVCLogRendererResultsListOfUsersFoundAttribute] = [userSet copy]; } #pragma mark - - (NSDictionary *)appKitAttributesFromRendererAttributes:(NSDictionary *)attributesIn { NSParameterAssert(attributesIn != nil); NSMutableDictionary *attributesOut = [NSMutableDictionary dictionary]; NSFont *defaultFont = self->_rendererAttributes[TVCLogRendererConfigurationAttributedStringPreferredFontAttribute]; NSColor *defaultFontColor = self->_rendererAttributes[TVCLogRendererConfigurationAttributedStringPreferredFontColorAttribute]; NSFont *boldItalicFont = defaultFont; if ([attributesIn containsKey:TVCLogRendererFormattingMonospaceTextAttribute]) { boldItalicFont = [RZFontManager() convertFont:boldItalicFont toFamily:@"Menlo"]; attributesOut[IRCTextFormatterMonospaceAttributeName] = @(YES); } if ([attributesIn containsKey:TVCLogRendererFormattingBoldTextAttribute]) { boldItalicFont = [RZFontManager() convertFont:boldItalicFont toHaveTrait:NSBoldFontMask]; attributesOut[IRCTextFormatterBoldAttributeName] = @(YES); } if ([attributesIn containsKey:TVCLogRendererFormattingItalicTextAttribute]) { boldItalicFont = [RZFontManager() convertFont:boldItalicFont toHaveTrait:NSItalicFontMask]; if ([boldItalicFont fontTraitSet:NSItalicFontMask] == NO) { boldItalicFont = boldItalicFont.convertToItalics; } attributesOut[IRCTextFormatterItalicAttributeName] = @(YES); } if (boldItalicFont) { attributesOut[NSFontAttributeName] = boldItalicFont; } if ([attributesIn containsKey:TVCLogRendererFormattingStrikethroughTextAttribute]) { attributesOut[NSStrikethroughStyleAttributeName] = @(NSUnderlineStyleSingle); attributesOut[IRCTextFormatterStrikethroughAttributeName] = @(YES); } if ([attributesIn containsKey:TVCLogRendererFormattingUnderlineTextAttribute]) { attributesOut[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle); attributesOut[IRCTextFormatterUnderlineAttributeName] = @(YES); } if ([attributesIn containsKey:TVCLogRendererFormattingForegroundColorAttribute]) { id foregroundColor = attributesIn[TVCLogRendererFormattingForegroundColorAttribute]; attributesOut[NSForegroundColorAttributeName] = [self.class mapColor:foregroundColor]; attributesOut[IRCTextFormatterForegroundColorAttributeName] = foregroundColor; } else { if (defaultFontColor) { attributesOut[NSForegroundColorAttributeName] = defaultFontColor; } } if ([attributesIn containsKey:TVCLogRendererFormattingBackgroundColorAttribute]) { id backgroundColor = attributesIn[TVCLogRendererFormattingBackgroundColorAttribute]; attributesOut[NSBackgroundColorAttributeName] = [self.class mapColor:backgroundColor]; attributesOut[IRCTextFormatterBackgroundColorAttributeName] = backgroundColor; } return [attributesOut copy]; } - (nullable NSString *)renderStringAsHTML:(NSString *)string withAttributes:(NSDictionary *)stringAttributes inRange:(NSRange)attributesRange isFirstFragment:(BOOL)isFirstFragment isLastFragment:(BOOL)isLastFragment { NSParameterAssert(string != nil); NSParameterAssert(stringAttributes != nil); NSString *html = nil; NSString *fragment = [string substringWithRange:attributesRange]; NSMutableDictionary *templateTokens = [NSMutableDictionary dictionary]; if ([stringAttributes containsKey:TVCLogRendererFormattingURLAttribute]) { AHHyperlinkScannerResult *link = stringAttributes[TVCLogRendererFormattingURLAttribute]; NSString *linkLocation = link.stringValue; if (self->_viewController.inlineMediaEnabledForView) { NSDictionary *linksMapped = self->_outputDictionary[TVCLogRendererResultsListOfLinksMappedInBodyAttribute]; NSString *uniqueIdentifier = linksMapped[linkLocation]; if (uniqueIdentifier) { templateTokens[@"anchorInlineMediaAvailable"] = @(YES); templateTokens[@"anchorInlineMediaUniqueID"] = uniqueIdentifier; } } templateTokens[@"anchorLocation"] = linkLocation; templateTokens[@"anchorTitle"] = [self.class escapeString:fragment]; html = [self.class renderTemplateNamed:@"renderedStandardAnchorLinkResource" attributes:templateTokens]; } else if ([stringAttributes containsKey:TVCLogRendererFormattingChannelNameAttribute]) { templateTokens[@"channelName"] = [self.class escapeString:fragment]; html = [self.class renderTemplateNamed:@"renderedChannelNameLinkResource" attributes:templateTokens]; } else if ([stringAttributes containsKey:TVCLogRendererFormattingConversationTrackingAttribute]) { if ([TPCPreferences disableNicknameColorHashing]) { templateTokens[@"inlineNicknameMatchFound"] = @(NO); } else { IRCChannel *channel = self->_viewController.associatedChannel; IRCChannelUser *member = [channel findMember:fragment]; NSString *nickname = member.user.nickname; if (nickname.length > 1) { NSString *modeSymbol = @""; if ([TPCPreferences conversationTrackingIncludesUserModeSymbol]) { NSString *modeSymbolTemp = member.mark; if (attributesRange.location > 0) { NSString *leftCharacter = [string stringCharacterAtIndex:(attributesRange.location - 1)]; if ([leftCharacter isEqualToString:modeSymbolTemp] == NO) { modeSymbol = modeSymbolTemp; } } else { modeSymbol = modeSymbolTemp; } } NSString *nicknameColorStyle = [IRCUserNicknameColorStyleGenerator nicknameColorStyleForString:nickname]; templateTokens[@"inlineNicknameMatchFound"] = @(YES); templateTokens[@"inlineNicknameColorStyle"] = nicknameColorStyle; templateTokens[@"inlineNicknameUserModeSymbol"] = modeSymbol; } } html = [self.class escapeString:fragment]; } BOOL escapeBody = self.escapeBody; templateTokens[@"messageFragmentEscaped"] = @(escapeBody); if (html == nil) { if (escapeBody) { html = [self.class escapeString:fragment]; } else { html = fragment; } } // --- // if (self->_renderedBodyOpenAttributes == nil) { self->_renderedBodyOpenAttributes = [NSMutableDictionary dictionary]; } void (^processToggleEffectsAttribute)(NSString *, NSString *) = ^(NSString *effectAttribute, NSString *effectTokenName) { if ([stringAttributes containsKey:effectAttribute]) { templateTokens[effectTokenName] = @(YES); // backwards compatibility if ([self->_renderedBodyOpenAttributes boolForKey:effectAttribute] == NO) { [self->_renderedBodyOpenAttributes setBool:YES forKey:effectAttribute]; NSString *openedTokenName = [NSString stringWithFormat:@"%@Opened", effectTokenName]; templateTokens[openedTokenName] = @(YES); } if (isLastFragment) { NSString *closedTokenName = [NSString stringWithFormat:@"%@ClosedAtEnd", effectTokenName]; templateTokens[closedTokenName] = @(YES); } } else { if ([self->_renderedBodyOpenAttributes boolForKey:effectAttribute]) { [self->_renderedBodyOpenAttributes removeObjectForKey:effectAttribute]; NSString *closedTokenName = [NSString stringWithFormat:@"%@ClosedAtStart", effectTokenName]; templateTokens[closedTokenName] = @(YES); } } }; processToggleEffectsAttribute(TVCLogRendererFormattingBoldTextAttribute, @"fragmentIsBold"); processToggleEffectsAttribute(TVCLogRendererFormattingItalicTextAttribute, @"fragmentIsItalicized"); processToggleEffectsAttribute(TVCLogRendererFormattingMonospaceTextAttribute, @"fragmentIsMonospace"); processToggleEffectsAttribute(TVCLogRendererFormattingStrikethroughTextAttribute, @"fragmentIsStruckthrough"); processToggleEffectsAttribute(TVCLogRendererFormattingUnderlineTextAttribute, @"fragmentIsUnderlined"); // --- // id foregroundColorNew = stringAttributes[TVCLogRendererFormattingForegroundColorAttribute]; id backgroundColorNew = stringAttributes[TVCLogRendererFormattingBackgroundColorAttribute]; id foregroundColorOld = self->_renderedBodyOpenAttributes[TVCLogRendererFormattingForegroundColorAttribute]; id backgroundColorOld = self->_renderedBodyOpenAttributes[TVCLogRendererFormattingBackgroundColorAttribute]; NSString *foregroundColor = nil; NSString *backgroundColor = nil; BOOL setNewColors = YES; if (foregroundColorOld || backgroundColorOld) { /* There is no need to open a new HTML segment if the color hasn't changed. */ if ([foregroundColorNew isEqual:foregroundColorOld] && [backgroundColorNew isEqual:backgroundColorOld]) { setNewColors = NO; } else { templateTokens[@"fragmentTextColorClosedAtStart"] = @(isFirstFragment == NO); templateTokens[@"fragmentTextColorClosedAtEnd"] = @(isLastFragment); } if (foregroundColorOld && foregroundColorNew == nil) { [self->_renderedBodyOpenAttributes removeObjectForKey:TVCLogRendererFormattingForegroundColorAttribute]; } if (backgroundColorOld && backgroundColorNew == nil) { [self->_renderedBodyOpenAttributes removeObjectForKey:TVCLogRendererFormattingBackgroundColorAttribute]; } } if (setNewColors && foregroundColorNew) { self->_renderedBodyOpenAttributes[TVCLogRendererFormattingForegroundColorAttribute] = foregroundColorNew; BOOL usesStyleTag = NO; foregroundColor = [self.class stringValueForColor:foregroundColorNew usesStyleTag:&usesStyleTag]; templateTokens[@"fragmentTextColorOpened"] = @(YES); templateTokens[@"fragmentForegroundColor"] = foregroundColor; templateTokens[@"fragmentForegroundColorIsSet"] = @(YES); templateTokens[@"fragmentTextColorUsesStyleTag"] = @(usesStyleTag); // backwards compatibility templateTokens[@"fragmentTextColorIsSet"] = @(YES); templateTokens[@"fragmentTextColor"] = templateTokens[@"fragmentForegroundColor"]; } if (setNewColors && backgroundColorNew) { self->_renderedBodyOpenAttributes[TVCLogRendererFormattingBackgroundColorAttribute] = backgroundColorNew; BOOL usesStyleTag = NO; backgroundColor = [self.class stringValueForColor:backgroundColorNew usesStyleTag:&usesStyleTag]; templateTokens[@"fragmentTextColorOpened"] = @(YES); templateTokens[@"fragmentBackgroundColor"] = backgroundColor; templateTokens[@"fragmentBackgroundColorIsSet"] = @(YES); templateTokens[@"fragmentTextColorUsesStyleTag"] = @(usesStyleTag); // backwards compatibility templateTokens[@"fragmentTextColorIsSet"] = @(YES); } if ([self isRenderingPRIVMSG_or_NOTICE]) { templateTokens[@"fragmentIsSpoiler"] = @(setNewColors && [foregroundColor isEqualToString:backgroundColor]); } else { templateTokens[@"fragmentIsSpoiler"] = @(NO); } // --- // /* Escape spaces that are prefix and suffix characters */ if (escapeBody) { if ([html hasPrefix:@" "]) { html = [html stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@" "]; } if ([html hasSuffix:@" "]) { html = [html stringByReplacingCharactersInRange:NSMakeRange((html.length - 1), 1) withString:@" "]; } } // --- // templateTokens[@"messageFragment"] = html; return [self.class renderTemplateNamed:@"formattedMessageFragment" attributes:templateTokens]; } #pragma mark - - (void)renderFinalResultsAsHTML { NSMutableString *finalResult = [NSMutableString string]; NSString *string = self->_bodyWithAttributes.string; NSUInteger stringLength = string.length; [self->_bodyWithAttributes enumerateAttributesInRange:NSMakeRange(0, stringLength) options:0 usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) { BOOL isFirstFragment = (range.location == 0); BOOL isLastFragment = ((range.location + range.length) == stringLength); NSString *html = [self renderStringAsHTML:string withAttributes:attributes inRange:range isFirstFragment:isFirstFragment isLastFragment:isLastFragment]; if (html) { [finalResult appendString:html]; } }]; self.finalResult = finalResult; } - (void)renderFinalResultsForAttributedBody { NSMutableAttributedString *finalResult = [self->_bodyWithAttributes mutableCopy]; NSString *string = self->_bodyWithAttributes.string; [self->_bodyWithAttributes enumerateAttributesInRange:NSMakeRange(0, string.length) options:0 usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) { NSDictionary *attributesToAdd = [self appKitAttributesFromRendererAttributes:attributes]; [finalResult addAttributes:attributesToAdd range:range]; }]; self.finalResult = finalResult; } #pragma mark - - (void)cleanupResources { self->_body = nil; self->_bodyWithAttributes = nil; self->_renderedBodyOpenAttributes = nil; self->_viewController = nil; } + (NSString *)renderBody:(NSString *)body forViewController:(TVCLogController *)viewController withAttributes:(NSDictionary *)inputDictionary resultInfo:(NSDictionary * _Nullable * _Nullable)outputDictionary { NSParameterAssert(body != nil); NSParameterAssert(viewController != nil); NSParameterAssert(inputDictionary != nil); if (body.length == 0) { return @""; } TVCLogLineType lineType = [inputDictionary unsignedIntegerForKey:TVCLogRendererConfigurationLineTypeAttribute]; TVCLogLineMemberType memberType = [inputDictionary unsignedIntegerForKey:TVCLogRendererConfigurationMemberTypeAttribute]; BOOL escapeBody = ([inputDictionary boolForKey:TVCLogRendererConfigurationDoNotEscapeBodyAttribute] == NO); TVCLogRenderer *renderer = [self new]; renderer.body = [THOPluginDispatcher willRenderMessage:body forViewController:viewController lineType:lineType memberType:memberType]; renderer.lineType = lineType; renderer.memberType = memberType; renderer.escapeBody = escapeBody; renderer.rendererAttributes = inputDictionary; renderer.viewController = viewController; /* Call -stripDangerousUnicodeCharactersFromBody first because it modifies the body. */ [renderer stripDangerousUnicodeCharactersFromBody]; [renderer buildEffectsDictionary]; [renderer buildListOfLinksInBody]; [renderer matchKeywords]; [renderer findAllChannelNames]; [renderer scanBodyForChannelMembers]; if ( outputDictionary) { *outputDictionary = [renderer.outputDictionary copy]; } [renderer renderFinalResultsAsHTML]; [renderer cleanupResources]; return renderer.finalResult; } + (NSAttributedString *)renderBodyAsAttributedString:(NSString *)body withAttributes:(NSDictionary *)inputDictionary { NSParameterAssert(body != nil); NSParameterAssert(inputDictionary != nil); if (body.length == 0) { return [NSAttributedString attributedString]; } NSFont *defaultFont = inputDictionary[TVCLogRendererConfigurationAttributedStringPreferredFontAttribute]; NSAssert((defaultFont != nil), @"FATAL ERROR: TVCLogRenderer cannot be supplied with a nil 'TVCLogRendererAttributedStringPreferredFontAttribute' attribute when rendering an attributed string"); TVCLogRenderer *renderer = [self new]; renderer.body = body; renderer.lineType = [inputDictionary unsignedIntegerForKey:TVCLogRendererConfigurationLineTypeAttribute]; renderer.memberType = [inputDictionary unsignedIntegerForKey:TVCLogRendererConfigurationMemberTypeAttribute]; renderer.rendererAttributes = inputDictionary; [renderer stripDangerousUnicodeCharactersFromBody]; [renderer buildEffectsDictionary]; [renderer renderFinalResultsForAttributedBody]; [renderer cleanupResources]; return renderer.finalResult; } #pragma mark - + (nullable NSString *)renderTemplateNamed:(NSString *)templateName { return [self renderTemplateNamed:templateName attributes:nil]; } + (nullable NSString *)renderTemplateNamed:(NSString *)templateName attributes:(nullable NSDictionary *)templateTokens { NSParameterAssert(templateName != nil); GRMustacheTemplate *template = [theme() templateWithName:templateName]; if (template == nil) { return nil; } return [self renderTemplate:template attributes:templateTokens]; } + (nullable NSString *)renderTemplate:(GRMustacheTemplate *)template { return [self renderTemplate:template attributes:nil]; } + (nullable NSString *)renderTemplate:(GRMustacheTemplate *)template attributes:(nullable NSDictionary *)templateTokens { NSParameterAssert(template != nil); NSString *templateRender = [template renderObject:templateTokens error:NULL]; if (templateRender == nil) { return nil; } return templateRender.removeAllNewlines; } + (NSString *)escapeHTML:(NSString *)html { NSParameterAssert(html != nil); NSString *stringEscaped = html.gtm_stringByEscapingForHTML; if (stringEscaped == nil) { stringEscaped = @""; } return stringEscaped; } + (NSString *)escapeStringSimple:(NSString *)string { NSParameterAssert(string != nil); string = [string stringByReplacingOccurrencesOfString:@"\t" withString:@"    "]; string = [string stringByReplacingOccurrencesOfString:@" " withString:@"  "]; return string; } + (NSString *)escapeString:(NSString *)string { NSParameterAssert(string != nil); NSString *stringEscaped = [self escapeHTML:string]; return [self escapeStringSimple:stringEscaped]; } + (nullable NSString *)stringValueForColor:(id)color usesStyleTag:(BOOL *)usesStyleTag { NSParameterAssert(color != nil); if ([color isKindOfClass:[NSColor class]]) { *usesStyleTag = YES; return [color hexadecimalValue]; } else if ([color isKindOfClass:[NSNumber class]]) { return [color stringValue]; } return nil; } + (nullable NSColor *)mapColor:(id)color { NSParameterAssert(color != nil); if ([color isKindOfClass:[NSColor class]]) { return color; } else if ([color isKindOfClass:[NSNumber class]]) { return [self mapColorCode:[color unsignedIntegerValue]]; } return nil; } + (NSColor *)mapColorCode:(NSUInteger)colorCode { NSParameterAssert(colorCode <= IRCTextFormatterEffectColorHighestDigit); return [NSColor formatterColors][colorCode]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCLogScriptEventSink.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #include #import "GTMEncodeHTML.h" #import "WebScriptObjectHelperPrivate.h" #import "NSObjectHelperPrivate.h" #import "TXMasterController.h" #import "TPCPreferencesLocal.h" #import "TPCThemeController.h" #import "TPCTheme.h" #import "THOPluginDispatcherPrivate.h" #import "THOPluginManagerPrivate.h" #import "THOPluginProtocolPrivate.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCUserNicknameColorStyleGeneratorPrivate.h" #import "IRCWorld.h" #import "TVCMainWindow.h" #import "TVCLogControllerPrivate.h" #import "TVCLogPolicyPrivate.h" #import "TVCLogRenderer.h" #import "TVCLogViewPrivate.h" #import "TVCLogViewInternalWK1.h" #import "TVCLogViewInternalWK2.h" #import "TVCLogScriptEventSinkPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCLogScriptEventSink () @property (nonatomic, weak) TVCLogView *webView; @end @interface TVCLogScriptEventSinkContext : NSObject @property (nonatomic, weak) TVCLogView *webView; @property (readonly) TVCLogPolicy *webViewPolicy; @property (readonly) TVCLogController *viewController; @property (readonly) IRCClient *associatedClient; @property (readonly, nullable) IRCChannel *associatedChannel; @property (nonatomic, copy) NSString *caller; @property (nonatomic, copy, nullable) NSArray *arguments; @property (nonatomic, copy, nullable) void (^completionBlock)(id _Nullable returnValue); @end @implementation TVCLogScriptEventSink - (instancetype)init { return [self initWithWebView:nil]; } - (instancetype)initWithWebView:(nullable TVCLogView *)webView { if ((self = [super init])) { self.webView = webView; return self; } return nil; } + (BOOL)isSelectorExcludedFromWebScript:(SEL)selector { if (selector == @selector(init) || selector == @selector(initWithWebView:) || selector == @selector(webView) || selector == @selector(webViewPolicy) || selector == @selector(associatedClient) || selector == @selector(associatedChannel) || selector == @selector(objectValueToCommon:) || selector == @selector(userContentController:didReceiveScriptMessage:) || selector == @selector(processInputData:inWebView:withSelector:) || selector == @selector(processInputData:inWebView:withSelector:minimumArgumentCount:withValidation:)) { return YES; } if ([NSStringFromSelector(selector) hasPrefix:@"_"]) { return NO; } return NO; } + (nullable NSString *)webScriptNameForSelector:(SEL)sel { return nil; } - (id)invokeUndefinedMethodFromWebScript:(NSString *)name withArguments:(NSArray *)arguments { SEL handlerSelector = NSSelectorFromString([name stringByAppendingString:@":inWebView:"]); if ([self respondsToSelector:handlerSelector] == NO) { return @(NO); } id argument = nil; if (arguments && arguments.count > 0) { argument = arguments[0]; TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN if ([argument isKindOfClass:[WebScriptObject class]]) { argument = [self.webView webScriptObjectToCommon:argument]; } TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END } NSMethodSignature *signature = [self methodSignatureForSelector:handlerSelector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self]; [invocation setSelector:handlerSelector]; [invocation setArgument:&argument atIndex:2]; TVCLogView *webView = self.webView; [invocation setArgument:&webView atIndex:3]; [invocation invoke]; return @(YES); } + (BOOL)isKeyExcludedFromWebScript:(const char *)name { return YES; } + (nullable NSString *)webScriptNameForKey:(const char *)name { return nil; } + (nullable id)objectValueToCommon:(id)object { if ([object isKindOfClass:[NSNull class]] || TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN [object isKindOfClass:[WebUndefined class]]) TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END { return nil; } if ([object isKindOfClass:[NSString class]]) { return [object gtm_stringByUnescapingFromHTML]; } return object; } + (NSString *)standardizeLineNumber:(NSString *)lineNumber { NSParameterAssert(lineNumber != nil); if ([lineNumber hasPrefix:@"line-"]) { return [lineNumber substringFromIndex:5]; } return lineNumber; } + (NSArray *)standardizeLineNumbers:(NSArray *)lineNumbers { NSParameterAssert(lineNumbers != nil); NSMutableArray *lineNumbersOut = [NSMutableArray arrayWithCapacity:lineNumbers.count]; for (NSString *lineNumber in lineNumbers) { [lineNumbersOut addObject: [self standardizeLineNumber:lineNumber]]; } return [lineNumbersOut copy]; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { NSString *handlerName = message.name; SEL handlerSelector = NSSelectorFromString([handlerName stringByAppendingString:@":inWebView:"]); if ([self respondsToSelector:handlerSelector] == NO) { return; } if ([self.class isSelectorExcludedFromWebScript:handlerSelector]) { return; } NSMethodSignature *signature = [self methodSignatureForSelector:handlerSelector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self]; [invocation setSelector:handlerSelector]; id body = message.body; [invocation setArgument:&body atIndex:2]; WKWebView *webView = message.webView; [invocation setArgument:&webView atIndex:3]; [invocation invoke]; } - (void)processInputData:(id)inputData forCaller:(NSString *)caller inWebView:(id)webView withSelector:(SEL)selector { [self processInputData:inputData forCaller:caller inWebView:webView withSelector:selector minimumArgumentCount:0 withValidation:nil]; } - (void)processInputData:(id)inputData forCaller:(NSString *)caller inWebView:(id)webView withSelector:(SEL)selector minimumArgumentCount:(NSUInteger)minimumArgumentCount withValidation:(BOOL (NS_NOESCAPE ^ _Nullable)(NSUInteger argumentIndex, id argument))validateArgumentBlock { TVCLogView *intWebView = nil; if ([webView isKindOfClass:[TVCLogView class]]) { intWebView = webView; } else if ([webView isKindOfClass:[TVCLogViewInternalWK1 class]] || [webView isKindOfClass:[TVCLogViewInternalWK2 class]]) { intWebView = [webView t_parentView]; } else { return; } /* -t_parentView in TVCLogViewInternalWK2 is maintained as a weak reference. That means that a race condition for WebKit2 is possible. A script event is received, is invoked on this function, and while it is waiting to be invoked, another thread deconstructs the parent view. */ if (intWebView == nil) { LogToConsoleFault("(intWebView == nil) condition faulted. \ Possible race condition. \ Invoking '%{public}@'", NSStringFromSelector(selector)); return; } NSInteger promiseIndex = (-1); NSArray *values = nil; /* Extract relevant information from inputData */ if ([inputData isKindOfClass:[NSDictionary class]]) { /* Check that the object exists in the dictionary before setting the value. If the object does not exist and we do not do this, then -integerValue will return 0 which is considered a valid promiseIndex value. */ id promiseIndexObj = [inputData valueForKey:@"promiseIndex"]; if (promiseIndexObj) { if ([promiseIndexObj isKindOfClass:[NSNumber class]] == NO) { [self.class throwJavaScriptException:@"'promiseIndex' must be a number" forCaller:caller inWebView:intWebView]; return; } promiseIndex = [promiseIndexObj integerValue]; } /* Values should always be in an array */ if (minimumArgumentCount > 0) { id valuesObj = [inputData valueForKey:@"values"]; if (valuesObj == nil || [valuesObj isKindOfClass:[NSArray class]] == NO) { [self.class throwJavaScriptException:@"'values' must be an array" forCaller:caller inWebView:intWebView]; return; } else { values = valuesObj; } } } else if ([inputData isKindOfClass:[NSString class]] || [inputData isKindOfClass:[NSNumber class]]) { if (minimumArgumentCount > 0) { values = @[inputData]; } } else if ([inputData isKindOfClass:[NSArray class]]) { if (minimumArgumentCount > 0) { values = inputData; } } else if ([inputData isKindOfClass:[NSNull class]] || TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN [inputData isKindOfClass:[WebUndefined class]]) TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END { if (minimumArgumentCount > 0) { values = @[[NSNull null]]; } } /* Perform validation if needed */ if (minimumArgumentCount > 0 && values.count < minimumArgumentCount) { [self.class throwJavaScriptException:@"Minimum number of arguments (%lu) condition not met" forCaller:caller inWebView:intWebView, minimumArgumentCount]; return; } if (validateArgumentBlock) { __block BOOL validationPassed = YES; [values enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { if (validateArgumentBlock(index, object) == NO) { validationPassed = NO; *stop = YES; } }]; if (validationPassed == NO) { [self.class throwJavaScriptException:@"Invalid argument type(s)" forCaller:caller inWebView:intWebView]; return; } } /* Pass validated data to selector */ TVCLogScriptEventSinkContext *context = [TVCLogScriptEventSinkContext new]; context.webView = intWebView; context.caller = caller; context.arguments = values; void (^completionBlock)(id) = nil; if (promiseIndex >= 0) { __weak typeof(intWebView) intWebViewWeak = intWebView; completionBlock = ^(id _Nullable returnValue) { if (returnValue == nil) { returnValue = [NSNull null]; } [intWebViewWeak evaluateFunction:@"appInternal.promiseKept" withArguments:@[@(promiseIndex), returnValue]]; }; } context.completionBlock = completionBlock; NSMethodSignature *signature = [self methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self]; [invocation setSelector:selector]; [invocation setArgument:&context atIndex:2]; [invocation invoke]; } + (void)logToJavaScriptConsole:(NSString *)message inWebView:(TVCLogView *)webView, ... { NSParameterAssert(message != nil); NSParameterAssert(webView != nil); va_list arguments; va_start(arguments, webView); [self logToJavaScriptConsole:message inWebView:webView withArguments:arguments]; va_end(arguments); } + (void)logToJavaScriptConsole:(NSString *)message inWebView:(TVCLogView *)webView withArguments:(va_list)arguments { NSParameterAssert(message != nil); NSParameterAssert(webView != nil); NSParameterAssert(arguments != NULL); NSString *messageFormatted = [[NSString alloc] initWithFormat:message arguments:arguments]; [webView evaluateFunction:@"console.log" withArguments:@[messageFormatted]]; } + (void)throwJavaScriptException:(NSString *)message inWebView:(TVCLogView *)webView, ... { NSParameterAssert(message != nil); NSParameterAssert(webView != nil); va_list arguments; va_start(arguments, webView); [self throwJavaScriptException:message forCaller:nil inWebView:webView withArguments:arguments]; va_end(arguments); } + (void)throwJavaScriptException:(NSString *)message forCaller:(nullable NSString *)caller inWebView:(TVCLogView *)webView, ... { NSParameterAssert(message != nil); NSParameterAssert(webView != nil); va_list arguments; va_start(arguments, webView); [self throwJavaScriptException:message forCaller:caller inWebView:webView withArguments:arguments]; va_end(arguments); } + (void)throwJavaScriptException:(NSString *)message forCaller:(nullable NSString *)caller inWebView:(TVCLogView *)webView withArguments:(va_list)arguments { NSParameterAssert(message != nil); NSParameterAssert(webView != nil); NSParameterAssert(arguments != NULL); NSString *messageFormatted = [[NSString alloc] initWithFormat:message arguments:arguments]; if (caller) { messageFormatted = [NSString stringWithFormat:@"Bridged function %@ returned error: %@", caller, messageFormatted]; } [webView evaluateFunction:@"console.error" withArguments:@[messageFormatted]]; } #pragma mark - #pragma mark Private Implementation - (void)channelIsActive:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.channelIsActive()" inWebView:webView withSelector:@selector(_channelIsActive:)]; } - (void)channelMemberCount:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.channelMemberCount()" inWebView:webView withSelector:@selector(_channelMemberCount:)]; } - (void)channelName:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.channelName()" inWebView:webView withSelector:@selector(_channelName:)]; } - (void)channelNameDoubleClicked:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.channelNameDoubleClicked()" inWebView:webView withSelector:@selector(_channelNameDoubleClicked:)]; } - (void)displayContextMenu:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.displayContextMenu()" inWebView:webView withSelector:@selector(_displayContextMenu:)]; } - (void)copySelectionWhenPermitted:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.copySelectionWhenPermitted()" inWebView:webView withSelector:@selector(_copySelectionWhenPermitted:)]; } - (void)encryptionAuthenticateUser:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.encryptionAuthenticateUser()" inWebView:webView withSelector:@selector(_encryptionAuthenticateUser:)]; } - (void)inlineMediaEnabledForView:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.inlineMediaEnabledForView()" inWebView:webView withSelector:@selector(_inlineMediaEnabledForView:)]; } - (void)loadInlineMedia:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.loadInlineMedia()" inWebView:webView withSelector:@selector(_loadInlineMedia:) minimumArgumentCount:4 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex <= 2) { return [argument isKindOfClass:[NSString class]]; } else { return [argument isKindOfClass:[NSNumber class]]; } }]; } - (void)localUserHostmask:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.localUserHostmask()" inWebView:webView withSelector:@selector(_localUserHostmask:)]; } - (void)localUserNickname:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.localUserNickname()" inWebView:webView withSelector:@selector(_localUserNickname:)]; } - (void)logToConsole:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.logToConsole()" inWebView:webView withSelector:@selector(_logToConsole:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return [argument isKindOfClass:[NSString class]]; }]; } - (void)networkName:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.networkName()" inWebView:webView withSelector:@selector(_networkName:)]; } - (void)nicknameColorStyleHash:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.nicknameColorStyleHash()" inWebView:webView withSelector:@selector(_nicknameColorStyleHash:) minimumArgumentCount:2 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return [argument isKindOfClass:[NSString class]]; }]; } - (void)nicknameDoubleClicked:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.nicknameDoubleClicked()" inWebView:webView withSelector:@selector(_nicknameDoubleClicked:)]; } - (void)notifyJumpToLineCallback:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.notifyJumpToLineCallback()" inWebView:webView withSelector:@selector(_notifyJumpToLineCallback:) minimumArgumentCount:2 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex == 0) { return [argument isKindOfClass:[NSString class]]; } else if (argumentIndex == 1 || argumentIndex == 2) { return [argument isKindOfClass:[NSNumber class]]; } return NO; }]; } - (void)notifyLinesAddedToView:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.notifyLinesAddedToView()" inWebView:webView withSelector:@selector(_notifyLinesAddedToView:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return ([argument isKindOfClass:[NSArray class]] || [argument isKindOfClass:[NSString class]]); }]; } - (void)notifyLinesRemovedFromView:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.notifyLinesRemovedFromView()" inWebView:webView withSelector:@selector(_notifyLinesRemovedFromView:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return ([argument isKindOfClass:[NSArray class]] || [argument isKindOfClass:[NSString class]]); }]; } - (void)printDebugInformation:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.printDebugInformation()" inWebView:webView withSelector:@selector(_printDebugInformation:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return [argument isKindOfClass:[NSString class]]; }]; } - (void)printDebugInformationToConsole:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.printDebugInformationToConsole()" inWebView:webView withSelector:@selector(_printDebugInformationToConsole:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return [argument isKindOfClass:[NSString class]]; }]; } - (void)renderMessagesBefore:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.renderMessagesBefore()" inWebView:webView withSelector:@selector(_renderMessagesBefore:) minimumArgumentCount:2 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex == 0) { return [argument isKindOfClass:[NSString class]]; } else if (argumentIndex == 1) { return [argument isKindOfClass:[NSNumber class]]; } return NO; }]; } - (void)renderMessagesAfter:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.renderMessagesAfter()" inWebView:webView withSelector:@selector(_renderMessagesAfter:) minimumArgumentCount:2 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex == 0) { return [argument isKindOfClass:[NSString class]]; } else if (argumentIndex == 1) { return [argument isKindOfClass:[NSNumber class]]; } return NO; }]; } - (void)renderMessagesInRange:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.renderMessagesInRange()" inWebView:webView withSelector:@selector(_renderMessagesInRange:) minimumArgumentCount:3 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex == 0 || argumentIndex == 1) { return [argument isKindOfClass:[NSString class]]; } else if (argumentIndex == 2) { return [argument isKindOfClass:[NSNumber class]]; } return NO; }]; } - (void)renderMessageWithSiblings:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.renderMessageWithSiblings()" inWebView:webView withSelector:@selector(_renderMessageWithSiblings:) minimumArgumentCount:3 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex == 0) { return [argument isKindOfClass:[NSString class]]; } else if (argumentIndex == 1 || argumentIndex == 2) { return [argument isKindOfClass:[NSNumber class]]; } return NO; }]; } - (void)retrievePreferencesWithMethodName:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.retrievePreferencesWithMethodName()" inWebView:webView withSelector:@selector(_retrievePreferencesWithMethodName:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return [argument isKindOfClass:[NSString class]]; }]; } - (void)renderTemplate:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.renderTemplate()" inWebView:webView withSelector:@selector(_renderTemplate:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex == 0) { return [argument isKindOfClass:[NSString class]]; } else if (argumentIndex == 1) { return ([argument isKindOfClass:[NSNull class]] || [argument isKindOfClass:[NSDictionary class]]); } return NO; }]; } - (void)sendPluginPayload:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.sendPluginPayload()" inWebView:webView withSelector:@selector(_sendPluginPayload:) minimumArgumentCount:2 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex == 0) { return [argument isKindOfClass:[NSString class]]; } else { return YES; } }]; } - (void)serverAddress:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.serverAddress()" inWebView:webView withSelector:@selector(_serverAddress:)]; } - (void)serverChannelCount:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.serverChannelCount()" inWebView:webView withSelector:@selector(_serverChannelCount:)]; } - (void)serverIsConnected:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.serverIsConnected()" inWebView:webView withSelector:@selector(_serverIsConnected:)]; } - (void)setAutomaticScrollingEnabled:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.setAutomaticScrollingEnabled()" inWebView:webView withSelector:@selector(_setAutomaticScrollingEnabled:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return ([argument isKindOfClass:[NSNumber class]]); }]; } - (void)setChannelName:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.setChannelName()" inWebView:webView withSelector:@selector(_setChannelName:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return ([argument isKindOfClass:[NSNull class]] || [argument isKindOfClass:[NSString class]]); }]; } - (void)setNickname:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.setNickname()" inWebView:webView withSelector:@selector(_setNickname:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return ([argument isKindOfClass:[NSNull class]] || [argument isKindOfClass:[NSString class]]); }]; } - (void)setSelection:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.setSelection()" inWebView:webView withSelector:@selector(_setSelection:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return ([argument isKindOfClass:[NSNull class]] || [argument isKindOfClass:[NSString class]]); }]; } - (void)setURLAddress:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.setURLAddress()" inWebView:webView withSelector:@selector(_setURLAddress:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return ([argument isKindOfClass:[NSNull class]] || [argument isKindOfClass:[NSString class]]); }]; } - (void)sidebarInversionIsEnabled:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.sidebarInversionIsEnabled()" inWebView:webView withSelector:@selector(_sidebarInversionIsEnabled:)]; } - (void)appearance:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.appearance()" inWebView:webView withSelector:@selector(_appearance:)]; } - (void)styleSettingsRetrieveValue:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.styleSettingsRetrieveValue()" inWebView:webView withSelector:@selector(_styleSettingsRetrieveValue:) minimumArgumentCount:1 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { return [argument isKindOfClass:[NSString class]]; }]; } - (void)styleSettingsSetValue:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.styleSettingsSetValue()" inWebView:webView withSelector:@selector(_styleSettingsSetValue:) minimumArgumentCount:2 withValidation:^BOOL(NSUInteger argumentIndex, id argument) { if (argumentIndex == 0) { return [argument isKindOfClass:[NSString class]]; } else { return ([argument isKindOfClass:[NSArray class]] || [argument isKindOfClass:[NSDictionary class]] || [argument isKindOfClass:[NSNull class]] || [argument isKindOfClass:[NSNumber class]] || [argument isKindOfClass:[NSString class]]); } }]; } - (void)topicBarDoubleClicked:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.topicBarDoubleClicked()" inWebView:webView withSelector:@selector(_topicBarDoubleClicked:)]; } - (void)finishedLayingOutView:(id)inputData inWebView:(id)webView { [self processInputData:inputData forCaller:@"app.finishedLayingOutView()" inWebView:webView withSelector:@selector(_finishedLayingOutView:)]; } #pragma mark - #pragma mark Private Implementation - (void)_channelIsActive:(TVCLogScriptEventSinkContext *)context { context.completionBlock( @(context.associatedChannel.isActive) ); } - (void)_channelMemberCount:(TVCLogScriptEventSinkContext *)context { IRCChannel *channel = context.associatedChannel; if (channel == nil || channel.isChannel == NO) { [self.class throwJavaScriptException:@"View is not a channel" forCaller:context.caller inWebView:context.webView]; return; } context.completionBlock( @(channel.numberOfMembers) ); } - (void)_channelName:(TVCLogScriptEventSinkContext *)context { context.completionBlock( context.associatedChannel.name ); } - (void)_channelNameDoubleClicked:(TVCLogScriptEventSinkContext *)context { [context.webViewPolicy channelNameDoubleClicked]; } - (void)_displayContextMenu:(TVCLogScriptEventSinkContext *)context { [context.webViewPolicy displayContextMenuInWebView:context.webView]; } - (void)_copySelectionWhenPermitted:(TVCLogScriptEventSinkContext *)context { if ([TPCPreferences copyOnSelect]) { NSString *selection = context.webView.selection; if (selection) { RZPasteboard().stringContent = selection; context.completionBlock( @(YES) ); } } context.completionBlock( @(NO) ); } - (void)_encryptionAuthenticateUser:(TVCLogScriptEventSinkContext *)context { #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 IRCClient *client = context.associatedClient; if (client.isLoggedIn == NO) { return; } IRCChannel *channel = context.associatedChannel; if (channel == nil || channel.isPrivateMessage == NO) { [self.class throwJavaScriptException:@"View is not a private message" forCaller:context.caller inWebView:context.webView]; return; } [client encryptionAuthenticateUser:channel.name]; #endif } - (void)_inlineMediaEnabledForView:(TVCLogScriptEventSinkContext *)context { context.completionBlock( @(context.viewController.inlineMediaEnabledForView) ); } - (void)_loadInlineMedia:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *address = [self.class objectValueToCommon:arguments[0]]; if (address.length == 0) { [self.class throwJavaScriptException:@"Length of address is 0" forCaller:context.caller inWebView:context.webView]; return; } NSString *uniqueIdentifier = [self.class objectValueToCommon:arguments[1]]; if (uniqueIdentifier.length == 0) { [self.class throwJavaScriptException:@"Length of unique identifier is 0" forCaller:context.caller inWebView:context.webView]; return; } NSString *lineNumber = [self.class objectValueToCommon:arguments[2]]; lineNumber = [self.class standardizeLineNumber:lineNumber]; if (lineNumber.length == 0) { [self.class throwJavaScriptException:@"Length of line number is 0" forCaller:context.caller inWebView:context.webView]; return; } NSNumber *index = [self.class objectValueToCommon:arguments[3]]; [context.viewController processInlineMediaAtAddress:address withUniqueIdentifier:uniqueIdentifier atLineNumber:lineNumber index:index.unsignedIntegerValue]; } - (void)_localUserHostmask:(TVCLogScriptEventSinkContext *)context { context.completionBlock( context.associatedClient.userHostmask ); } - (void)_localUserNickname:(TVCLogScriptEventSinkContext *)context { context.completionBlock( context.associatedClient.userNickname ); } - (void)_logToConsole:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *message = [self.class objectValueToCommon:arguments[0]]; LogToConsole("JavaScript: %{public}@", message); } - (void)_networkName:(TVCLogScriptEventSinkContext *)context { context.completionBlock( context.associatedClient.networkName ); } - (void)_nicknameColorStyleHash:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *inputString = [self.class objectValueToCommon:arguments[0]]; NSString *colorStyle = [self.class objectValueToCommon:arguments[1]]; TPCThemeSettingsNicknameColorStyle colorStyleEnum; if ([colorStyle isEqualToString:@"HSL-dark"]) { colorStyleEnum = TPCThemeSettingsNicknameColorStyleDark; } else if ([colorStyle isEqualToString:@"HSL-light"]) { colorStyleEnum = TPCThemeSettingsNicknameColorStyleLight; } else { [self.class throwJavaScriptException:@"Invalid style" forCaller:context.caller inWebView:context.webView]; return; } context.completionBlock( [IRCUserNicknameColorStyleGenerator hashForString:inputString colorStyle:colorStyleEnum] ); } - (void)_nicknameDoubleClicked:(TVCLogScriptEventSinkContext *)context { [context.webViewPolicy nicknameDoubleClicked]; } - (void)_notifyJumpToLineCallback:(TVCLogScriptEventSinkContext *)context { void (^contextCompletionBlock)(id _Nullable) = context.completionBlock; NSArray *arguments = context.arguments; NSString *lineNumber = [self.class objectValueToCommon:arguments[0]]; lineNumber = [self.class standardizeLineNumber:lineNumber]; if (lineNumber.length == 0) { [self.class throwJavaScriptException:@"Length of line number is 0" forCaller:context.caller inWebView:context.webView]; contextCompletionBlock(nil); return; } BOOL successful = [[self.class objectValueToCommon:arguments[1]] boolValue]; BOOL scrolledToBottom = [[self.class objectValueToCommon:arguments[2]] boolValue]; [context.viewController notifyJumpToLine:lineNumber successful:successful scrolledToBottom:scrolledToBottom]; } - (void)_notifyLinesAddedToView:(TVCLogScriptEventSinkContext *)context { [self _notifyLinesAdded:YES context:context]; } - (void)_notifyLinesRemovedFromView:(TVCLogScriptEventSinkContext *)context { [self _notifyLinesAdded:NO context:context]; } - (void)_notifyLinesAdded:(BOOL)added context:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; id lineNumbersUncut = [self.class objectValueToCommon:arguments[0]]; if ([lineNumbersUncut isKindOfClass:[NSString class]]) { lineNumbersUncut = @[lineNumbersUncut]; } NSArray *lineNumbers = [self.class standardizeLineNumbers:lineNumbersUncut]; if (added) { [context.viewController notifyLinesAddedToView:[lineNumbers copy]]; } else { [context.viewController notifyLinesRemovedFromView:[lineNumbers copy]]; } } - (void)_printDebugInformation:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *message = [self.class objectValueToCommon:arguments[0]]; [context.associatedClient printDebugInformation:message inChannel:context.associatedChannel]; } - (void)_printDebugInformationToConsole:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *message = [self.class objectValueToCommon:arguments[0]]; [context.associatedClient printDebugInformationToConsole:message]; } - (void)_renderMessagesBefore:(TVCLogScriptEventSinkContext *)context { [self _renderMessagesAfter:NO context:context]; } - (void)_renderMessagesAfter:(TVCLogScriptEventSinkContext *)context { [self _renderMessagesAfter:YES context:context]; } - (void)_renderMessagesAfter:(BOOL)after context:(TVCLogScriptEventSinkContext *)context { void (^contextCompletionBlock)(id _Nullable) = context.completionBlock; NSArray *arguments = context.arguments; NSString *lineNumber = [self.class objectValueToCommon:arguments[0]]; lineNumber = [self.class standardizeLineNumber:lineNumber]; if (lineNumber.length == 0) { [self.class throwJavaScriptException:@"Length of line number is 0" forCaller:context.caller inWebView:context.webView]; contextCompletionBlock(nil); return; } NSInteger maximumNumberOfLines = [[self.class objectValueToCommon:arguments[1]] integerValue]; if (maximumNumberOfLines <= 0) { [self.class throwJavaScriptException:@"Maximum number of lines must be equal to 1 or greater" forCaller:context.caller inWebView:context.webView]; contextCompletionBlock(nil); return; } void (^renderCompletionBlock)(NSArray *) = ^(NSArray *> *renderedLogLines) { contextCompletionBlock(renderedLogLines); }; if (after == NO) { [context.viewController renderLogLinesBeforeLineNumber:lineNumber maximumNumberOfLines:maximumNumberOfLines completionBlock:renderCompletionBlock]; } else { [context.viewController renderLogLinesAfterLineNumber:lineNumber maximumNumberOfLines:maximumNumberOfLines completionBlock:renderCompletionBlock]; } } - (void)_renderMessagesInRange:(TVCLogScriptEventSinkContext *)context { void (^contextCompletionBlock)(id _Nullable) = context.completionBlock; NSArray *arguments = context.arguments; NSString *lineNumberAfter = [self.class objectValueToCommon:arguments[0]]; NSString *lineNumberBefore = [self.class objectValueToCommon:arguments[1]]; lineNumberAfter = [self.class standardizeLineNumber:lineNumberAfter]; lineNumberBefore = [self.class standardizeLineNumber:lineNumberBefore]; if (lineNumberAfter.length == 0 || lineNumberBefore.length == 0) { [self.class throwJavaScriptException:@"Length of line number is 0" forCaller:context.caller inWebView:context.webView]; contextCompletionBlock(nil); return; } NSInteger maximumNumberOfLines = [[self.class objectValueToCommon:arguments[2]] integerValue]; if (maximumNumberOfLines < 0) { [self.class throwJavaScriptException:@"Maximum number of lines must be equal to 0 or greater" forCaller:context.caller inWebView:context.webView]; contextCompletionBlock(nil); return; } void (^renderCompletionBlock)(NSArray *) = ^(NSArray *> *renderedLogLines) { contextCompletionBlock(renderedLogLines); }; [context.viewController renderLogLinesAfterLineNumber:lineNumberAfter beforeLineNumber:lineNumberBefore maximumNumberOfLines:maximumNumberOfLines completionBlock:renderCompletionBlock]; } - (void)_renderMessageWithSiblings:(TVCLogScriptEventSinkContext *)context { void (^contextCompletionBlock)(id _Nullable) = context.completionBlock; NSArray *arguments = context.arguments; NSString *lineNumber = [self.class objectValueToCommon:arguments[0]]; lineNumber = [self.class standardizeLineNumber:lineNumber]; if (lineNumber.length == 0) { [self.class throwJavaScriptException:@"Length of line number is 0" forCaller:context.caller inWebView:context.webView]; contextCompletionBlock(nil); return; } NSInteger numberOfLinesBefore = [[self.class objectValueToCommon:arguments[1]] integerValue]; NSInteger numberOfLinesAfter = [[self.class objectValueToCommon:arguments[2]] integerValue]; if (numberOfLinesBefore < 0 || numberOfLinesAfter < 0) { [self.class throwJavaScriptException:@"Number of lines must be equal to 0 or greater" forCaller:context.caller inWebView:context.webView]; contextCompletionBlock(nil); return; } void (^renderCompletionBlock)(NSArray *) = ^(NSArray *> *renderedLogLines) { contextCompletionBlock(renderedLogLines); }; [context.viewController renderLogLineAtLineNumber:lineNumber numberOfLinesBefore:numberOfLinesBefore numberOfLinesAfter:numberOfLinesAfter completionBlock:renderCompletionBlock]; } - (void)_renderTemplate:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *templateName = [self.class objectValueToCommon:arguments[0]]; if (templateName.length == 0) { [self.class throwJavaScriptException:@"Length of template name is 0" forCaller:context.caller inWebView:context.webView]; context.completionBlock(nil); return; } NSDictionary *templateAttributes = [self.class objectValueToCommon:arguments[1]]; NSString *renderedTemplate = [TVCLogRenderer renderTemplateNamed:templateName attributes:templateAttributes]; context.completionBlock( renderedTemplate ); } - (void)_retrievePreferencesWithMethodName:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *methodName = [self.class objectValueToCommon:arguments[0]]; SEL methodSelector = NSSelectorFromString(methodName); NSMethodSignature *methodSignature = [TPCPreferences methodSignatureForSelector:methodSelector]; if (methodSignature == nil) { [self.class throwJavaScriptException:@"Unknown method named: '%@'" forCaller:context.caller inWebView:context.webView]; context.completionBlock(nil); return; } else if (strcmp(methodSignature.methodReturnType, @encode(void)) == 0) { [self.class throwJavaScriptException:@"Method named '%@' does not return a value" forCaller:context.caller inWebView:context.webView]; context.completionBlock(nil); return; } NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; invocation.target = [TPCPreferences class]; invocation.selector = methodSelector; [invocation invoke]; void *returnValue; [invocation getReturnValue:&returnValue]; context.completionBlock( [NSValue valueWithPrimitive:returnValue withType:methodSignature.methodReturnType] ); } - (void)_sendPluginPayload:(TVCLogScriptEventSinkContext *)context { if ([sharedPluginManager() supportsFeature:THOPluginItemSupportedFeatureWebViewJavaScriptPayloads] == NO) { [self.class throwJavaScriptException:@"There are no plugins loaded that support JavaScript payloads" forCaller:context.caller inWebView:context.webView]; return; } NSArray *arguments = context.arguments; NSString *payloadLabel = [self.class objectValueToCommon:arguments[0]]; if (payloadLabel.length == 0) { [self.class throwJavaScriptException:@"Length of payload label is 0" forCaller:context.caller inWebView:context.webView]; return; } id payloadContents = [self.class objectValueToCommon:arguments[1]]; THOPluginWebViewJavaScriptPayloadConcreteObject *payloadObject = [THOPluginWebViewJavaScriptPayloadConcreteObject new]; payloadObject.payloadLabel = payloadLabel; payloadObject.payloadContents = payloadContents; [THOPluginDispatcher didReceiveJavaScriptPayload:payloadObject fromViewController:context.viewController]; } - (void)_serverAddress:(TVCLogScriptEventSinkContext *)context { context.completionBlock( context.associatedClient.serverAddress ); } - (void)_serverChannelCount:(TVCLogScriptEventSinkContext *)context { context.completionBlock( @(context.associatedClient.channelCount) ); } - (void)_serverIsConnected:(TVCLogScriptEventSinkContext *)context { context.completionBlock( @(context.associatedClient.isLoggedIn) ); } - (void)_setAutomaticScrollingEnabled:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; BOOL enabled = [[self.class objectValueToCommon:arguments[0]] boolValue]; [context.webView setAutomaticScrollingEnabled:enabled]; } - (void)_setChannelName:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *value = [self.class objectValueToCommon:arguments[0]]; context.webViewPolicy.channelName = value; } - (void)_setNickname:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *value = [self.class objectValueToCommon:arguments[0]]; context.webViewPolicy.nickname = value; } - (void)_setSelection:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *selection = [self.class objectValueToCommon:arguments[0]]; if (selection && selection.length == 0) { selection = nil; } context.webView.selection = selection; } - (void)_setURLAddress:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *value = [self.class objectValueToCommon:arguments[0]]; context.webViewPolicy.anchorURL = value; } - (void)_sidebarInversionIsEnabled:(TVCLogScriptEventSinkContext *)context { TVCMainWindowAppearance *appearance = context.viewController.attachedWindow.userInterfaceObjects; context.completionBlock( @(appearance.isDarkAppearance) ); } - (void)_appearance:(TVCLogScriptEventSinkContext *)context { TVCMainWindowAppearance *appearance = context.viewController.attachedWindow.userInterfaceObjects; context.completionBlock( appearance.shortAppearanceDescription ); } - (void)_styleSettingsRetrieveValue:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *keyName = [self.class objectValueToCommon:arguments[0]]; NSString *errorValue = nil; id result = [themeSettings() styleSettingsRetrieveValueForKey:keyName error:&errorValue]; if (errorValue) { [self.class throwJavaScriptException:errorValue forCaller:context.caller inWebView:context.webView]; } context.completionBlock( result ); } - (void)_styleSettingsSetValue:(TVCLogScriptEventSinkContext *)context { NSArray *arguments = context.arguments; NSString *keyName = [self.class objectValueToCommon:arguments[0]]; id keyValue = [self.class objectValueToCommon:arguments[1]]; NSString *errorValue = nil; BOOL result = [themeSettings() styleSettingsSetValue:keyValue forKey:keyName error:&errorValue]; if (errorValue) { [self.class throwJavaScriptException:errorValue forCaller:context.caller inWebView:context.webView]; } if (result) { [worldController() evaluateFunctionOnAllViews:@"Textual.styleSettingDidChange" arguments:@[keyName]]; } context.completionBlock( @(result) ); } - (void)_topicBarDoubleClicked:(TVCLogScriptEventSinkContext *)context { [context.webViewPolicy topicBarDoubleClicked]; } - (void)_finishedLayingOutView:(TVCLogScriptEventSinkContext *)context { [context.webView setViewFinishedLayout]; } @end #pragma mark - @implementation TVCLogScriptEventSinkContext - (TVCLogController *)viewController { return self.webView.viewController; } - (TVCLogPolicy *)webViewPolicy { return self.webView.webViewPolicy; } - (IRCClient *)associatedClient { return self.viewController.associatedClient; } - (nullable IRCChannel *)associatedChannel { return self.viewController.associatedChannel; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCLogView.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TXMasterController.h" #import "TPCApplicationInfo.h" #import "TPCThemeControllerPrivate.h" #import "TPCPathInfo.h" #import "TPCPreferencesLocal.h" #import "TVCLogControllerPrivate.h" #import "TVCLogScriptEventSinkPrivate.h" #import "TVCLogViewPrivate.h" #import "TVCLogViewInternalWK1.h" #import "TVCLogViewInternalWK2.h" #import "TVCMainWindowPrivate.h" #import "WebScriptObjectHelperPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCLogView () @property (nonatomic, strong) id webViewBacking; @property (nonatomic, readwrite, assign) BOOL isUsingWebKit2; @property (nonatomic, getter=isLayingOutView, readwrite) BOOL layingOutView; @end @implementation TVCLogView NSString * const TVCLogViewCommonUserAgentString = @"Textual/1.0 (+https://help.codeux.com/textual/Inline-Media-Scanner-User-Agent.kb)"; - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithViewController:(TVCLogController *)viewController { NSParameterAssert(viewController != nil); if ((self = [super init])) { self.viewController = viewController; [self constructWebView]; return self; } return nil; } - (void)dealloc { self.webViewBacking = nil; } + (BOOL)webKit2Enabled { if ([TVCLogViewInternalWK2 t_safeToUse] == NO) { return NO; } return [TPCPreferences webKit2Enabled]; } - (void)constructWebView { BOOL isUsingWebKit2 = [self.class webKit2Enabled]; self.isUsingWebKit2 = isUsingWebKit2; if (isUsingWebKit2) { self.webViewBacking = [[TVCLogViewInternalWK2 alloc] initWithHostView:self]; } else { self.webViewBacking = [[TVCLogViewInternalWK1 alloc] initWithHostView:self]; } } - (void)copyContentString { [self stringByEvaluatingFunction:@"Textual.documentHTML" completionHandler:^(NSString *result) { RZPasteboard().stringContent = result; }]; } - (BOOL)hasSelection { NSString *selection = self.selection; return (selection.length > 0); } - (void)clearSelection { [self evaluateFunction:@"Textual.clearSelection"]; } - (void)print { // Printing is probably broken: [self.webView print:nil]; } - (BOOL)keyDown:(NSEvent *)e inView:(NSView *)view { NSParameterAssert(e != nil); NSParameterAssert(view != nil); NSUInteger m = e.modifierFlags; BOOL cmd = ((m & NSEventModifierFlagCommand) == NSEventModifierFlagCommand); BOOL alt = ((m & NSEventModifierFlagOption) == NSEventModifierFlagOption); BOOL ctrl = ((m & NSEventModifierFlagControl) == NSEventModifierFlagControl); if (ctrl == NO && alt == NO && cmd == NO) { [self.viewController logViewWebViewKeyDown:e]; return YES; } return NO; } - (BOOL)performDragOperation:(id )sender { NSParameterAssert(sender != nil); NSURL *fileURL = [NSURL URLFromPasteboard:[sender draggingPasteboard]]; if (fileURL) { NSString *filename = fileURL.path; [self.viewController logViewWebViewReceivedDropWithFile:filename]; } return NO; } - (void)informDelegateWebViewFinishedLoading { [self.viewController logViewWebViewFinishedLoading]; } - (void)informDelegateWebViewClosedUnexpectedly { [self.viewController logViewWebViewClosedUnexpectedly]; } - (void)setViewFinishedLayout { self.layingOutView = NO; } - (TVCLogPolicy *)webViewPolicy { return [self.webViewBacking webViewPolicy]; } - (NSView *)webView { return self.webViewBacking; } @end #pragma mark - @implementation TVCLogView (TVCLogViewBackingProxy) + (void)emptyCaches { [TVCLogViewInternalWK1 emptyCaches]; [TVCLogViewInternalWK2 emptyCaches]; } - (void)recreateTemporaryCopyOfThemeIfNecessary { if (mainWindow().reloadingTheme) { return; } if ([TPCApplicationInfo timeIntervalSinceApplicationLaunch] < (2 * 60)) { return; } [themeController() recreateTemporaryCopyOfThemeIfNecessary]; } - (void)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL { NSParameterAssert(string != nil); NSParameterAssert(baseURL != nil); self.layingOutView = YES; [self _loadHTMLString:string baseURL:baseURL]; } - (void)_loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL { NSParameterAssert(string != nil); NSParameterAssert(baseURL != nil); TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN if (self.isUsingWebKit2 == NO) { WebFrame *webViewFrame = [self.webViewBacking mainFrame]; [webViewFrame loadHTMLString:string baseURL:baseURL]; return; } TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END [self recreateTemporaryCopyOfThemeIfNecessary]; WKWebView *webView = self.webViewBacking; NSString *filename = [NSString stringWithFormat:@"%@.html", [NSString stringWithUUID]]; NSURL *filePath = [baseURL URLByAppendingPathComponent:filename]; NSError *fileWriteError = nil; if ([string writeToURL:filePath atomically:NO encoding:NSUTF8StringEncoding error:&fileWriteError] == NO) { LogToConsoleError("Failed to write temporary file: %{public}@", fileWriteError.localizedDescription); return; } [webView loadFileURL:filePath allowingReadAccessToURL:themeController().temporaryURL]; } - (void)stopLoading { TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN if (self.isUsingWebKit2 == NO) { WebFrame *webViewFrame = [self.webViewBacking mainFrame]; [webViewFrame stopLoading]; return; } TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END WKWebView *webView = self.webViewBacking; [webView stopLoading]; } - (void)findString:(NSString *)searchString movingForward:(BOOL)movingForward { NSParameterAssert(searchString != nil); [self.webViewBacking findString:searchString movingForward:movingForward]; } - (void)enableOffScreenUpdates { // XRPerformBlockAsynchronouslyOnMainQueue(^{ [(id)self.webView enableOffScreenUpdates]; // }); } - (void)disableOffScreenUpdates { // XRPerformBlockAsynchronouslyOnMainQueue(^{ [(id)self.webView disableOffScreenUpdates]; // }); } - (void)redrawViewIfNeeded { XRPerformBlockSynchronouslyOnMainQueue(^{ [(id)self.webView redrawViewIfNeeded]; }); } - (void)redrawView { XRPerformBlockSynchronouslyOnMainQueue(^{ [(id)self.webView redrawView]; }); } - (void)resetScrollerPosition { XRPerformBlockSynchronouslyOnMainQueue(^{ [(id)self.webView resetScrollerPosition]; }); } - (void)resetScrollerPositionTo:(BOOL)scrolledToBottom { XRPerformBlockSynchronouslyOnMainQueue(^{ [(id)self.webView resetScrollerPositionTo:scrolledToBottom]; }); } - (void)saveScrollerPosition { XRPerformBlockSynchronouslyOnMainQueue(^{ [(id)self.webView saveScrollerPosition]; }); } - (void)restoreScrollerPosition { XRPerformBlockSynchronouslyOnMainQueue(^{ [(id)self.webView restoreScrollerPosition]; }); } - (void)setAutomaticScrollingEnabled:(BOOL)automaticScrollingEnabled { // XRPerformBlockAsynchronouslyOnMainQueue(^{ [(id)self.webView setAutomaticScrollingEnabled:automaticScrollingEnabled]; // }); } @end #pragma mark - @implementation TVCLogView (TVCLogViewJavaScriptHandler) - (void)evaluateJavaScript:(NSString *)code { [self evaluateJavaScript:code completionHandler:nil]; } - (void)evaluateJavaScript:(NSString *)code completionHandler:(void (^ _Nullable)(id _Nullable result))completionHandler { NSParameterAssert(code != nil); dispatch_block_t blockToPerform = ^{ [self.webViewBacking _t_evaluateJavaScript:code completionHandler:completionHandler]; }; // if (self.isUsingWebKit2) { // blockToPerform(); // } else { XRPerformBlockAsynchronouslyOnMainQueue(blockToPerform); // } } + (NSString *)descriptionOfJavaScriptResult:(id)scriptResult { NSParameterAssert(scriptResult != nil); if ([scriptResult isKindOfClass:[NSString class]]) { return scriptResult; } else if ([scriptResult isKindOfClass:[NSArray class]] || [scriptResult isKindOfClass:[NSDictionary class]]) { return [scriptResult description]; } else if ([scriptResult isKindOfClass:[NSNumber class]]) { if ([scriptResult isBooleanValue]) { if ([scriptResult boolValue]) { return @"true"; } else { return @"false"; } } else { return [scriptResult stringValue]; } } else if ([scriptResult isKindOfClass:[NSNull class]]) { return @"null"; } else { return @"undefined"; } } + (NSString *)escapeJavaScriptString:(NSString *)string { NSParameterAssert(string != nil); NSString *escapedString = string; escapedString = [escapedString stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; escapedString = [escapedString stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; escapedString = [escapedString stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; escapedString = [escapedString stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; return escapedString; } - (void)evaluateFunction:(NSString *)function { [self evaluateFunction:function withArguments:nil completionHandler:nil]; } - (void)evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments { [self evaluateFunction:function withArguments:arguments completionHandler:nil]; } - (void)evaluateFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(id _Nullable result))completionHandler { NSParameterAssert(function != nil); NSString *compiledScript = [self compiledFunctionCall:function withArguments:arguments]; [self evaluateJavaScript:compiledScript completionHandler:completionHandler]; } - (void)booleanByEvaluatingFunction:(NSString *)function completionHandler:(void (^ _Nullable)(BOOL result))completionHandler { [self booleanByEvaluatingFunction:function withArguments:nil completionHandler:completionHandler]; } - (void)booleanByEvaluatingFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(BOOL result))completionHandler { [self evaluateFunction:function withArguments:arguments completionHandler:^(id result) { BOOL resultBool = NO; if (result && [result isKindOfClass:[NSNumber class]]) { resultBool = [result boolValue]; } if (completionHandler) { completionHandler(resultBool); } }]; } - (void)stringByEvaluatingFunction:(NSString *)function completionHandler:(void (^ _Nullable)(NSString * _Nullable result))completionHandler { [self stringByEvaluatingFunction:function withArguments:nil completionHandler:completionHandler]; } - (void)stringByEvaluatingFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(NSString * _Nullable result))completionHandler { [self evaluateFunction:function withArguments:arguments completionHandler:^(id result) { NSString *resultString = nil; if (result && [result isKindOfClass:[NSString class]]) { resultString = result; } if (completionHandler) { completionHandler(resultString); } }]; } - (void)arrayByEvaluatingFunction:(NSString *)function completionHandler:(void (^ _Nullable)(NSArray * _Nullable result))completionHandler { [self arrayByEvaluatingFunction:function withArguments:nil completionHandler:completionHandler]; } - (void)arrayByEvaluatingFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(NSArray * _Nullable result))completionHandler { [self evaluateFunction:function withArguments:arguments completionHandler:^(id result) { NSArray *resultArray = nil; if (result && [result isKindOfClass:[NSArray class]]) { resultArray = result; } if (completionHandler) { completionHandler(resultArray); } }]; } - (void)dictionaryByEvaluatingFunction:(NSString *)function completionHandler:(void (^ _Nullable)(NSDictionary * _Nullable result))completionHandler { [self dictionaryByEvaluatingFunction:function withArguments:nil completionHandler:completionHandler]; } - (void)dictionaryByEvaluatingFunction:(NSString *)function withArguments:(nullable NSArray *)arguments completionHandler:(void (^ _Nullable)(NSDictionary * _Nullable result))completionHandler { [self evaluateFunction:function withArguments:arguments completionHandler:^(id result) { NSDictionary *resultDictionary = nil; if (result && [result isKindOfClass:[NSDictionary class]]) { resultDictionary = result; } if (completionHandler) { completionHandler(resultDictionary); } }]; } - (void)logToJavaScriptConsole:(NSString *)message, ... { NSParameterAssert(message != nil); va_list arguments; va_start(arguments, message); [TVCLogScriptEventSink logToJavaScriptConsole:message inWebView:self withArguments:arguments]; va_end(arguments); } @end #pragma mark - @implementation TVCLogView (TVCLogViewJavaScriptHandlerPrivate) - (NSString *)compileJavaScriptDictionaryArgument:(NSDictionary *)objects { NSParameterAssert(objects != nil); NSMutableString *compiledScript = [NSMutableString string]; [compiledScript appendString:@"{"]; NSInteger lastIndex = (objects.count - 1); __block NSInteger currentIndex = 0; [objects enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop) { /* Perform check to make sure the key we are using is actually a string. */ if ([key isKindOfClass:[NSString class]] == NO) { LogToConsoleDebug("Silently ignoring non-string key: %{public}@", NSStringFromClass([key class])); return; } /* Add key and value to new object. */ NSString *keyString = [self.class escapeJavaScriptString:key]; NSString *objectString = [self compileJavaScriptGenericArgument:object]; if (currentIndex == lastIndex) { [compiledScript appendFormat:@"\"%@\":%@", keyString, objectString]; } else { [compiledScript appendFormat:@"\"%@\":%@, ", keyString, objectString]; } currentIndex += 1; }]; [compiledScript appendString:@"}"]; return [compiledScript copy]; } - (NSString *)compileJavaScriptArrayArgument:(NSArray *)objects { NSParameterAssert(objects != nil); NSMutableString *compiledScript = [NSMutableString string]; [compiledScript appendString:@"["]; NSInteger lastIndex = (objects.count - 1); [objects enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) { NSString *objectString = [self compileJavaScriptGenericArgument:object]; if (index == lastIndex) { [compiledScript appendString:objectString]; } else { [compiledScript appendFormat:@"%@,", objectString]; } }]; [compiledScript appendString:@"]"]; return [compiledScript copy]; } - (NSString *)compileJavaScriptGenericArgument:(id)object { NSParameterAssert(object != nil); if ([object isKindOfClass:[NSURL class]]) { object = [object absoluteString]; } if ([object isKindOfClass:[NSString class]]) { NSString *objectEscaped = [self.class escapeJavaScriptString:object]; return [NSString stringWithFormat:@"\"%@\"", objectEscaped]; } else if ([object isKindOfClass:[NSNumber class]]) { if ([object isBooleanValue]) { if ([object boolValue]) { return @"true"; } else { return @"false"; } } else { return [object stringValue]; } } else if ([object isKindOfClass:[NSArray class]]) { return [self compileJavaScriptArrayArgument:object]; } else if ([object isKindOfClass:[NSDictionary class]]) { return [self compileJavaScriptDictionaryArgument:object]; } else if ([object isKindOfClass:[NSNull class]]) { return @"null"; } else { return @"undefined"; } } - (NSString *)compiledFunctionCall:(NSString *)function withArguments:(nullable NSArray *)arguments { NSParameterAssert(function != nil); NSMutableString *compiledScript = [NSMutableString string]; [compiledScript appendFormat:@"%@(", function]; if ( arguments) { NSUInteger argumentCount = arguments.count; for (NSUInteger i = 0; i < argumentCount; i++) { NSString *argument = [self compileJavaScriptGenericArgument:arguments[i]]; [compiledScript appendString:argument]; if (i < (argumentCount - 1)) { [compiledScript appendString:@","]; } } } [compiledScript appendString:@");\n"]; return [compiledScript copy]; } TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN - (id)webScriptObjectToCommon:(WebScriptObject *)object { NSParameterAssert(object != nil); NSAssert((self.isUsingWebKit2 == NO), @"Cannot use feature when WebKit2 is in use"); WebFrame *webViewFrame = [self.webViewBacking mainFrame]; JSGlobalContextRef jsContextRef = webViewFrame.globalContext; return [object toCommonInContext:jsContextRef]; } TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCLogViewInternalWK1.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN #include #import "TPCThemeController.h" #import "TPCTheme.h" #import "TVCLogController.h" #import "TVCLogPolicyPrivate.h" #import "TVCLogScriptEventSinkPrivate.h" #import "TVCLogViewPrivate.h" #import "TVCWK1AutoScrollerPrivate.h" #import "TVCLogViewInternalWK1.h" NS_ASSUME_NONNULL_BEGIN @interface TVCLogViewInternalWK1 () @property (nonatomic, strong) TVCWK1AutoScroller *autoScroller; @property (nonatomic, readwrite, strong) TVCLogScriptEventSink *webViewScriptSink; @end static WebPreferences *_sharedWebViewPreferences = nil; static TVCLogPolicy *_sharedWebPolicy = nil; @implementation TVCLogViewInternalWK1 #pragma mark - #pragma mark Factory + (void)_t_initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedWebViewPreferences = [[WebPreferences alloc] initWithIdentifier:@"TVCLogViewInternalWK1SharedWebPreferencesObject"]; _sharedWebViewPreferences.cacheModel = WebCacheModelDocumentViewer; _sharedWebViewPreferences.usesPageCache = NO; SEL selector = @selector(setShouldRespectImageOrientation:); if ([_sharedWebViewPreferences respondsToSelector:selector]) { NSMethodSignature *signature = [_sharedWebViewPreferences methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:_sharedWebViewPreferences]; [invocation setSelector:selector]; BOOL yes = YES; [invocation setArgument:&yes atIndex:2]; [invocation invoke]; } _sharedWebPolicy = [TVCLogPolicy new]; }); } - (instancetype)initWithHostView:(TVCLogView *)hostView { [self.class _t_initialize]; if ((self = [self initWithFrame:NSZeroRect])) { [self constructWebViewWithHostView:hostView]; return self; } return nil; } - (void)constructWebViewWithHostView:(TVCLogView *)hostView { NSParameterAssert(hostView != nil); self.t_parentView = hostView; TVCLogScriptEventSink *webViewScriptSink = [[TVCLogScriptEventSink alloc] initWithWebView:hostView]; self.webViewScriptSink = webViewScriptSink; self.preferences = _sharedWebViewPreferences; self.translatesAutoresizingMaskIntoConstraints = NO; self.customUserAgent = TVCLogViewCommonUserAgentString; self.frameLoadDelegate = (id)self; self.policyDelegate = (id)self; self.resourceLoadDelegate = (id)self; self.UIDelegate = (id)self; self.shouldUpdateWhileOffscreen = NO; [self updateBackgroundColor]; } - (void)dealloc { self.frameLoadDelegate = nil; self.policyDelegate = nil; self.resourceLoadDelegate = nil; self.UIDelegate = nil; } - (TVCLogPolicy *)webViewPolicy { return _sharedWebPolicy; } #pragma mark - #pragma mark View Events - (void)keyDown:(NSEvent *)e { if ([self.t_parentView keyDown:e inView:self]) { return; } [super keyDown:e]; } - (BOOL)performDragOperation:(id )sender { return [self.t_parentView performDragOperation:sender]; } #pragma mark - #pragma mark Utilities + (void)emptyCaches { LogToConsoleDebug("WebKit cache cleared"); } - (void)updateBackgroundColor { NSColor *windowColor = themeSettings().underlyingWindowColor; if (windowColor == nil) { windowColor = [NSColor blackColor]; } [(id)self setBackgroundColor:windowColor]; } - (void)maybeInformDelegateWebViewFinishedLoading { if (self.t_viewHasLoaded == NO || self.t_viewHasScriptObject == NO) { return; } [self.t_parentView performSelectorInCommonModes:@selector(informDelegateWebViewFinishedLoading) withObject:nil afterDelay:1.2]; [self constructAutoScroller]; } - (void)findString:(NSString *)searchString movingForward:(BOOL)movingForward { NSParameterAssert(searchString != nil); [self searchFor:searchString direction:movingForward caseSensitive:NO wrap:YES]; } #pragma mark - #pragma mark View Configuration - (BOOL)maintainsInactiveSelection { return YES; } #pragma mark - #pragma mark JavaScript - (void)_t_evaluateJavaScript:(NSString *)code completionHandler:(void (^ _Nullable)(id _Nullable))completionHandler { NSParameterAssert(code != nil); WebScriptObject *scriptObject = self.windowScriptObject; if (scriptObject == nil || [scriptObject isKindOfClass:[WebUndefined class]]) { if (completionHandler) { completionHandler(nil); return; } } id scriptResult = [scriptObject evaluateWebScript:code]; if (scriptResult) { if ([scriptResult isKindOfClass:[NSNull class]] || [scriptResult isKindOfClass:[WebUndefined class]]) { if (completionHandler) { completionHandler(nil); return; } } else if ([scriptResult isKindOfClass:[WebScriptObject class]]) { scriptResult = [self.t_parentView webScriptObjectToCommon:scriptResult]; } } if (completionHandler) { completionHandler(scriptResult); } } #pragma mark - #pragma mark Scroll View - (void)enableOffScreenUpdates { self.shouldUpdateWhileOffscreen = YES; } - (void)disableOffScreenUpdates { self.shouldUpdateWhileOffscreen = NO; } - (void)redrawViewIfNeeded { /* The WebView is layer backed which means it is not redrawn unless it is told to do so. TVCWK1AutoScroller automatically tells it to do so if it scrolled programmatically or by the end user. When there is not enough content to scroll, the WebView is not redrawn because there is never a scroll event triggered. Therefore, this call exists to tell TVCWK1AutoScroller that we are interested in a redraw and it will then take appropriate actions depending on whether one is necessary or not. */ if (self.t_parentView.viewController.visible == NO) { return; } if (self.autoScroller.canScroll == NO) { [self.autoScroller redrawFrame]; } } - (void)redrawView { if (self.t_parentView.viewController.visible == NO) { return; } [self.autoScroller redrawFrame]; } - (void)resetScrollerPosition { [self.autoScroller restoreScrollerPosition]; } - (void)resetScrollerPositionTo:(BOOL)scrolledToBottom { [self.autoScroller resetScrollerPositionTo:scrolledToBottom]; } - (void)saveScrollerPosition { [self.autoScroller saveScrollerPosition]; } - (void)restoreScrollerPosition { [self.autoScroller restoreScrollerPosition]; } - (void)constructAutoScroller { WebFrameView *frameView = self.mainFrame.frameView; self.autoScroller = [[TVCWK1AutoScroller alloc] initWitFrameView:frameView]; } - (void)setAutomaticScrollingEnabled:(BOOL)automaticScrollingEnabled { self.autoScroller.automaticScrollingEnabled = automaticScrollingEnabled; } #pragma mark - #pragma mark Web View Delegate - (NSArray *)webView:(WebView *)webView contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems { NSParameterAssert(webView == self); return [_sharedWebPolicy webView1:webView logView:self.t_parentView contextMenuWithDefaultMenuItems:defaultMenuItems]; } - (NSUInteger)webView:(WebView *)webView dragDestinationActionMaskForDraggingInfo:(id)draggingInfo { NSParameterAssert(webView == self); return [_sharedWebPolicy webView1:webView logView:self.t_parentView dragDestinationActionMaskForDraggingInfo:draggingInfo]; } - (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id )listener { NSParameterAssert(webView == self); [_sharedWebPolicy webView1:webView logView:self.t_parentView decidePolicyForNavigationAction:actionInformation request:request frame:frame decisionListener:listener]; } - (void)webView:(WebView *)webView resource:(id)identifier didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge fromDataSource:(WebDataSource *)dataSource { NSParameterAssert(webView == self); [_sharedWebPolicy webView1:webView logView:self.t_parentView resource:identifier didReceiveAuthenticationChallenge:challenge fromDataSource:dataSource]; } - (void)webView:(WebView *)webView didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame { NSParameterAssert(webView == self); self.t_viewHasScriptObject = YES; [windowObject setValue:self.webViewScriptSink forKey:@"TextualScriptSink"]; [self maybeInformDelegateWebViewFinishedLoading]; } - (void)webView:(WebView *)webView didFinishLoadForFrame:(WebFrame *)frame { NSParameterAssert(webView == self); self.t_viewHasLoaded = YES; [self maybeInformDelegateWebViewFinishedLoading]; [self updateBackgroundColor]; } @end NS_ASSUME_NONNULL_END TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCLogViewInternalWK2.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "WKWebViewPrivate.h" #include #include #import "IRCChannel.h" #import "TPCPreferencesLocal.h" #import "TVCLogController.h" #import "TVCLogPolicyPrivate.h" #import "TVCLogScriptEventSinkPrivate.h" #import "TVCLogViewPrivate.h" #import "TVCLogViewInternalWK2.h" NS_ASSUME_NONNULL_BEGIN #define _maximumProcessCount 8 #define _maximumViewInstances 50 @interface TVCLogViewInternalWK2 () @property (nonatomic, assign) BOOL t_observingLoadingProperty; @end @implementation TVCLogViewInternalWK2 static WKProcessPool *_sharedProcessPool = nil; static WKUserContentController *_sharedUserContentController = nil; static WKWebViewConfiguration *_sharedWebViewConfiguration = nil; static TVCLogPolicy *_sharedWebPolicy = nil; static TVCLogScriptEventSink *_sharedWebViewScriptSink = nil; static BOOL _safeToUseWebKit2 = YES; static NSUInteger _numberOfViews = 0; #pragma mark - #pragma mark Factory + (void)_t_initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self constructProcessPool]; _sharedWebViewConfiguration = [WKWebViewConfiguration new]; _sharedWebViewConfiguration.processPool = _sharedProcessPool; _sharedWebViewConfiguration._allowUniversalAccessFromFileURLs = YES; WKPreferences *preferences = _sharedWebViewConfiguration.preferences; preferences._allowFileAccessFromFileURLs = YES; preferences._developerExtrasEnabled = YES; _sharedWebViewScriptSink = [[TVCLogScriptEventSink alloc] initWithWebView:nil]; _sharedUserContentController = [WKUserContentController new]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"appearance"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"channelIsActive"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"channelMemberCount"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"channelName"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"channelNameDoubleClicked"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"displayContextMenu"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"copySelectionWhenPermitted"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"encryptionAuthenticateUser"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"inlineMediaEnabledForView"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"loadInlineMedia"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"localUserHostmask"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"localUserNickname"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"logToConsole"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"networkName"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"nicknameColorStyleHash"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"nicknameDoubleClicked"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"notifyLinesAddedToView"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"notifyLinesRemovedFromView"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"notifyJumpToLineCallback"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"printDebugInformation"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"printDebugInformationToConsole"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"renderMessagesBefore"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"renderMessagesAfter"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"renderMessagesInRange"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"renderMessageWithSiblings"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"renderTemplate"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"retrievePreferencesWithMethodName"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"sendPluginPayload"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"serverAddress"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"serverChannelCount"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"serverIsConnected"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"setChannelName"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"setNickname"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"setSelection"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"setURLAddress"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"sidebarInversionIsEnabled"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"styleSettingsRetrieveValue"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"styleSettingsSetValue"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"topicBarDoubleClicked"]; [_sharedUserContentController addScriptMessageHandler:(id)_sharedWebViewScriptSink name:@"finishedLayingOutView"]; _sharedWebViewConfiguration.userContentController = _sharedUserContentController; _sharedWebPolicy = [TVCLogPolicy new]; }); } + (void)constructProcessPool { /* What we are doing here is very dirty which means it is probably a good idea that we go above and beyond for error checking incase this stuff is changed. */ WKProcessPool *sharedProcessPool = [WKProcessPool alloc]; if ([TPCPreferences webKit2ProcessPoolSizeLimited] == NO) { goto create_normal_pool; } if ([sharedProcessPool respondsToSelector:@selector(_initWithConfiguration:)] == NO) { goto create_normal_pool; } Class processPoolConfigurationClass = objc_getClass("_WKProcessPoolConfiguration"); if (processPoolConfigurationClass) { id processPoolConfiguration = [processPoolConfigurationClass new]; if (processPoolConfiguration == nil) { goto create_normal_pool; } else if ([processPoolConfiguration respondsToSelector:@selector(setMaximumProcessCount:)] == NO) { goto create_normal_pool; } [processPoolConfiguration setMaximumProcessCount:_maximumProcessCount]; _sharedProcessPool = [sharedProcessPool _initWithConfiguration:processPoolConfiguration]; return; } create_normal_pool: _sharedProcessPool = [sharedProcessPool init]; } - (instancetype)initWithHostView:(TVCLogView *)hostView { [self.class _t_initialize]; if ((self = [self initWithFrame:NSZeroRect configuration:_sharedWebViewConfiguration])) { [self constructWebViewWithHostView:hostView]; // It's not critical that this is thread safe _numberOfViews += 1; return self; } return nil; } - (void)constructWebViewWithHostView:(TVCLogView *)hostView { NSParameterAssert(hostView != nil); self.t_parentView = hostView; self.allowsBackForwardNavigationGestures = NO; self.allowsMagnification = NO; self.translatesAutoresizingMaskIntoConstraints = NO; self.allowsLinkPreview = [TPCPreferences webKit2PreviewLinks]; self.customUserAgent = TVCLogViewCommonUserAgentString; self.navigationDelegate = (id)self; self.UIDelegate = (id)self; } - (void)dealloc { _numberOfViews -= 1; self.navigationDelegate = nil; self.UIDelegate = nil; } - (TVCLogPolicy *)webViewPolicy { return _sharedWebPolicy; } + (BOOL)t_safeToUse { /* June 2024: WebKit2 was enabled by default for beta update users. One user who is in 200+ channels managed to enter WK2 into an endless termination loop. Probably resource exhaustion. I am still investigating the underlying cause of that. But given the extremes of the situation, this temporary fix may just end up being a permanent one. */ if (_numberOfViews > _maximumViewInstances) { return NO; } return _safeToUseWebKit2; } #pragma mark - #pragma mark Load Overrides - (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL { [self startObservingLoadingProperty]; return [super loadFileURL:URL allowingReadAccessToURL:readAccessURL]; } - (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL { [self startObservingLoadingProperty]; return [super loadHTMLString:string baseURL:baseURL]; } - (void)stopLoading { [self stopObservingLoadingProperty]; [super stopLoading]; } #pragma mark - #pragma mark View Events - (void)keyDown:(NSEvent *)e { if ([self.t_parentView keyDown:e inView:self]) { return; } [super keyDown:e]; } - (BOOL)performDragOperation:(id )sender { return [self.t_parentView performDragOperation:sender]; } #pragma mark - #pragma mark Utilities + (void)emptyCaches { WKWebsiteDataStore *wk2WebsiteDataStore = _sharedWebViewConfiguration.websiteDataStore; if (wk2WebsiteDataStore == nil) { return; } NSSet *itemsToRemove = [NSSet setWithArray:@[ WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache ]]; [wk2WebsiteDataStore removeDataOfTypes:itemsToRemove modifiedSince:[NSDate distantPast] completionHandler:^{ LogToConsoleDebug("WebKit2 cache cleared"); }]; } - (void)findString:(NSString *)searchString movingForward:(BOOL)movingForward { NSParameterAssert(searchString != nil); _WKFindOptions findOptions = (_WKFindOptionsCaseInsensitive | _WKFindOptionsShowOverlay | _WKFindOptionsShowFindIndicator | _WKFindOptionsWrapAround); if (movingForward == NO) { findOptions |= _WKFindOptionsBackwards; } SEL selector = @selector(_findString:options:maxCount:); if ([self respondsToSelector:selector]) { NSMethodSignature *signature = [self methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:self]; [invocation setSelector:selector]; [invocation setArgument:&searchString atIndex:2]; [invocation setArgument:&findOptions atIndex:3]; NSUInteger one = 1; [invocation setArgument:&one atIndex:4]; [invocation invoke]; } } - (void)startObservingLoadingProperty { if (self.t_observingLoadingProperty) { return; } [self addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:NULL]; self.t_observingLoadingProperty = YES; } - (void)stopObservingLoadingProperty { if (self.t_observingLoadingProperty == NO) { return; } [self removeObserver:self forKeyPath:@"loading"]; self.t_observingLoadingProperty = NO; } - (void)maybeInformDelegateWebViewFinishedLoading { if (self.t_viewIsLoading == NO && self.t_viewIsNavigating == NO) { [self stopObservingLoadingProperty]; [self.t_parentView performSelectorInCommonModes:@selector(informDelegateWebViewFinishedLoading) withObject:nil afterDelay:1.2]; } } - (void)webViewClosedUnexpectedly { [self.t_parentView informDelegateWebViewClosedUnexpectedly]; } #pragma mark - #pragma mark View Configuration - (BOOL)maintainsInactiveSelection { return YES; } #pragma mark - #pragma mark JavaScript - (void)logEvaluateJavaScriptError:(NSError *)error { NSParameterAssert(error != nil); NSNumber *lineNumber = error.userInfo[@"WKJavaScriptExceptionLineNumber"]; NSString *errorMessage = error.userInfo[@"WKJavaScriptExceptionMessage"]; NSURL *sourceURL = error.userInfo[@"WKJavaScriptExceptionSourceURL"]; NSString *channelName = self.t_parentView.viewController.associatedChannel.name; if (channelName == nil) { channelName = @"Server Console"; } if (lineNumber == nil || errorMessage == nil || sourceURL == nil) { LogToConsoleError("JavaScript Error in %{private}@: %{public}@", channelName, error.localizedDescription); return; } LogToConsoleError("A JavaScript error occurred in %{public}@ on line %{public}ld of %{public}@: %{public}@", channelName, lineNumber.unsignedIntegerValue, sourceURL.standardizedTildePath, errorMessage); } - (void)_t_evaluateJavaScript:(NSString *)code completionHandler:(void (^ _Nullable)(id _Nullable))completionHandler { NSParameterAssert(code != nil); [self evaluateJavaScript:code completionHandler:^(id result, NSError *error) { if (error) { [self logEvaluateJavaScriptError:error]; } if (result) { if ([result isKindOfClass:[NSNull class]] || TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN [result isKindOfClass:[WebUndefined class]]) TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END { if (completionHandler) { completionHandler(nil); } return; } } if (completionHandler) { completionHandler(result); } }]; } #pragma mark - #pragma mark Scroll View - (void)enableOffScreenUpdates { } - (void)disableOffScreenUpdates { } - (void)redrawViewIfNeeded { } - (void)redrawView { } - (void)resetScrollerPosition { } - (void)resetScrollerPositionTo:(BOOL)scrolledToBottom { } - (void)saveScrollerPosition { } - (void)restoreScrollerPosition { } - (void)setAutomaticScrollingEnabled:(BOOL)automaticScrollingEnabled { } #pragma mark - #pragma mark Web View Delegate - (void)_webView:(WKWebView *)webView webContentProcessDidTerminateWithReason:(_WKProcessTerminationReason)reason { NSParameterAssert(webView == self); switch (reason) { case _WKProcessTerminationReasonExceededMemoryLimit: LogToConsoleError("WebView [%{public}@] terminated due to memory limit", self.description); break; case _WKProcessTerminationReasonExceededCPULimit: LogToConsoleError("WebView [%{public}@] terminated due to CPU limit", self.description); break; case _WKProcessTerminationReasonRequestedByClient: LogToConsoleDebug("WebView [%{public}@] terminated by client", self.description); break; case _WKProcessTerminationReasonCrash: LogToConsoleError("WebView [%{public}@] terminated due to crash", self.description); break; default: LogToConsoleError("WebView [%{public}@] terminated by other means: %{public}ld", self.description, reason); break; } if (reason == _WKProcessTerminationReasonRequestedByClient) { return; } LogToConsoleError("A WebKit process terminated for a reason not understood. Disabling WebKit2 until relaunch."); LogStackTrace(); _safeToUseWebKit2 = NO; [self webViewClosedUnexpectedly]; } - (void)_webViewWebProcessDidBecomeUnresponsive:(WKWebView *)webView { NSParameterAssert(webView == self); LogToConsoleError("WebView [%{public}@] terminated due to unresponsive", self.description); } - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { NSParameterAssert(webView == self); LogToConsoleDebug("WebView [%{public}@] terminated", self.description); } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { NSParameterAssert(object == self); if ([keyPath isEqualToString:@"loading"]) { self.t_viewIsLoading = self.loading; [self maybeInformDelegateWebViewFinishedLoading]; } } - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSParameterAssert(webView == self); [_sharedWebPolicy webView2:webView logView:self.t_parentView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; } - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler { NSParameterAssert(webView == self); [_sharedWebPolicy webView2:webView logView:self.t_parentView didReceiveAuthenticationChallenge:challenge completionHandler:completionHandler]; } - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { NSParameterAssert(webView == self); self.t_viewIsNavigating = YES; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { NSParameterAssert(webView == self); self.t_viewIsNavigating = NO; [self maybeInformDelegateWebViewFinishedLoading]; } - (NSMenu *)_webView:(WKWebView *)webView contextMenu:(NSMenu *)menu forElement:(id)element { NSParameterAssert(webView == self); return [_sharedWebPolicy webView2:webView logView:self.t_parentView contextMenuWithDefaultMenu:menu]; } @end #pragma mark - #pragma mark WKView Swizzle /* I am not proud of this, but you have to admit, WebKit2 API is very limited... */ @implementation NSView (WKiewSwizzle) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ XRExchangeInstanceMethod(@"WKView", @"performDragOperation:", @"__t_priv_performDragOperation:"); }); } - (BOOL)__t_priv_performDragOperation:(id )sender { /* Override drag and drop to allow files to be sent to a user instead of WebKit thinking that it should load the file as a resource. */ NSView *superview = self.superview; if ([superview respondsToSelector:@selector(performDragOperation:)]) { return [superview performDragOperation:sender]; } return [self __t_priv_performDragOperation:sender]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Channel View/TVCWK1AutoScroller.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN #import "TVCWK1AutoScrollerPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCWK1AutoScroller () { CGFloat _scrollHeightCurrentValue; CGFloat _scrollHeightPreviousValue; CGFloat _scrollPositionCurrentValue; CGFloat _scrollPositionPreviousValue; BOOL _userScrolled; BOOL _scrolledUpwards; BOOL _restoreScrollerPosition; /* NSRect _lastFrame; */ } @property (nonatomic, weak) WebFrameView *frameView; @property (readonly) NSView *documentView; @end @implementation TVCWK1AutoScroller /* Maximum distance user can scroll up before automatic scrolling is disabled. */ static CGFloat _userScrolledMinimum = 25.0; - (instancetype)initWitFrameView:(WebFrameView *)frameView { NSParameterAssert(frameView != nil); if ((self = [super init])) { self.frameView = frameView; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { WebFrameView *frameView = self.frameView; NSView *documentView = frameView.documentView; [RZNotificationCenter() addObserver:self selector:@selector(webViewDidChangeFrame:) name:NSViewFrameDidChangeNotification object:frameView]; [RZNotificationCenter() addObserver:self selector:@selector(webViewDidChangeFrame:) name:NSViewFrameDidChangeNotification object:documentView]; [RZNotificationCenter() addObserver:self selector:@selector(webViewDidChangeBounds:) name:NSViewBoundsDidChangeNotification object:documentView.enclosingScrollView.contentView]; self->_automaticScrollingEnabled = YES; self->_scrollPositionCurrentValue = 0.0; self->_scrollPositionPreviousValue = 0.0; self->_userScrolled = NO; self->_restoreScrollerPosition = YES; } - (NSView *)documentView { return self.frameView.documentView; } - (BOOL)viewingBottom { if (self->_userScrolled == NO) { return YES; } return [self viewIsScrolledToBottom:self.documentView]; } - (BOOL)viewIsScrolledToBottom:(NSView *)aView { NSRect visibleRect = aView.visibleRect; CGFloat scrollHeight = (aView.frame.size.height - visibleRect.size.height); CGFloat scrollPosition = visibleRect.origin.y; return CGFloatAreEqual(scrollHeight, scrollPosition); } - (void)resetScrollerPosition { [self resetScrollerPositionTo:NO]; } - (void)resetScrollerPositionTo:(BOOL)scrolledToBottom { self->_restoreScrollerPosition = scrolledToBottom; } - (void)saveScrollerPosition { self->_restoreScrollerPosition = self.viewingBottom; } - (void)restoreScrollerPosition { if (self->_restoreScrollerPosition) { self->_restoreScrollerPosition = NO; } else { return; } [self scrollViewToBottom:self.documentView]; } - (void)scrollViewToBottom:(NSView *)aView { NSRect visibleRect = aView.visibleRect; visibleRect.origin.y = (aView.frame.size.height - visibleRect.size.height); [aView scrollRectToVisible:visibleRect]; } - (void)dealloc { [RZNotificationCenter() removeObserver:self]; self.frameView = nil; } - (BOOL)canScroll { WebFrameView *frameView = self.frameView; NSRect frameRect = frameView.frame; NSRect contentRect = frameView.documentView.frame; return (contentRect.size.height > frameRect.size.height); } - (void)redrawFrame { [self.documentView setNeedsLayout:YES]; } - (void)webViewDidChangeBounds:(NSNotification *)aNotification { /* Context */ NSView *documentView = self.documentView; NSRect visibleRect = documentView.visibleRect; /* The maximum scrollPosition can equal. The bottom */ CGFloat scrollHeightCurrent = (documentView.frame.size.height - visibleRect.size.height); CGFloat scrollHeightPrevious = self->_scrollHeightPreviousValue; /* The current position. When at bottom, will == scrollHeight */ CGFloat scrollPositionCurrent = visibleRect.origin.y; CGFloat scrollPositionPrevious = self->_scrollPositionPreviousValue; /* If nothing changed, we ignore the event. It is possible to receive a scroll event but nothing changes because we ignore elastic scrolling. User can reach bottom, elastic scroll, then bounce back. We get notification for both times we reach bottom, but values do not change. */ if (CGFloatAreEqual(scrollHeightPrevious, scrollHeightCurrent) && CGFloatAreEqual(scrollPositionPrevious, scrollPositionCurrent)) { return; } /* Even if user is elastic scrolling, we want to record the latest scroll height values. */ self->_scrollHeightPreviousValue = scrollHeightPrevious; self->_scrollHeightCurrentValue = scrollHeightCurrent; /* Ignore elastic scrolling */ if (scrollPositionCurrent < 0 || scrollPositionCurrent > scrollHeightCurrent) { return; } /* Only record scroll position changes if we weren't elastic scrolling. */ self->_scrollPositionPreviousValue = scrollPositionPrevious; self->_scrollPositionCurrentValue = scrollPositionCurrent; /* Scrolled upwards? */ BOOL scrolledUpwards = (scrollPositionCurrent < scrollPositionPrevious); self->_scrolledUpwards = scrolledUpwards; /* User scrolled above bottom? */ BOOL userScrolled = ((scrollHeightCurrent - scrollPositionCurrent) > _userScrolledMinimum); if (self->_userScrolled != userScrolled) { self->_userScrolled = userScrolled; if (userScrolled) { LogToConsoleDebug("User scrolled above threshold. Disabled auto scroll"); } else { LogToConsoleDebug("Scrolled below threshold. Enabled auto scroll"); } } } - (void)webViewDidChangeFrame:(NSNotification *)aNotification { /* Never scroll if user scrolled up */ if (self->_automaticScrollingEnabled == NO || self->_userScrolled) { return; } [self scrollViewToBottom:self.documentView]; } @end NS_ASSUME_NONNULL_END TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END ================================================ FILE: Sources/App/Classes/Views/Errors/TVCAlert.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. Please see Acknowledgements.pdf for additional informative. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TLOLocalization.h" #import "TVCAlert.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TVCAlertLaunchedAs) { TVCAlertLaunchedAsNonblockingPanel = 0, TVCAlertLaunchedAsModal, TVCAlertLaunchedAsSheet }; @interface TVCAlert () @property (nonatomic, strong, readwrite) IBOutlet NSPanel *panel; @property (nonatomic, weak) IBOutlet NSImageView *iconImageView; @property (nonatomic, weak) IBOutlet NSImageView *warningIconImageView; @property (nonatomic, weak) IBOutlet NSTextField *messageTextField; @property (nonatomic, weak) IBOutlet NSTextField *informativeTextField; @property (nonatomic, weak) IBOutlet NSButton *firstButton; @property (nonatomic, weak) IBOutlet NSButton *secondButton; @property (nonatomic, weak) IBOutlet NSButton *thirdButton; @property (nonatomic, weak, readwrite) IBOutlet NSButton *suppressionButton; @property (nonatomic, assign) BOOL alertFinished; @property (nonatomic, assign) BOOL alertImmutable; @property (nonatomic, assign) BOOL alertVisible; @property (nonatomic, assign) BOOL layoutPerformed; @property (nonatomic, assign) TVCAlertLaunchedAs launchedAs; @property (nonatomic, copy, nullable) TVCAlertCompletionBlock completionBlock; @property (nonatomic, copy, nullable) TVCAlertButtonClickedBlock firstButtonAction; @property (nonatomic, copy, nullable) TVCAlertButtonClickedBlock secondButtonAction; @property (nonatomic, copy, nullable) TVCAlertButtonClickedBlock thirdButtonAction; - (IBAction)buttonPressed:(id)sender; @end @implementation TVCAlert - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TVCAlert" owner:self topLevelObjects:nil]; self.panel.floatingPanel = YES; LogToConsoleDebug("[%{public}@] Creating alert host", self); } - (void)showAlert { [self showAlertWithCompletionBlock:nil]; } - (void)showAlertWithCompletionBlock:(nullable TVCAlertCompletionBlock)completionBlock { [self _showAlertInWindow:nil withCompletionBlock:completionBlock]; } - (void)showAlertInWindow:(NSWindow *)window { NSParameterAssert(window != nil); [self showAlertInWindow:window withCompletionBlock:nil]; } - (void)showAlertInWindow:(NSWindow *)window withCompletionBlock:(nullable TVCAlertCompletionBlock)completionBlock { NSParameterAssert(window != nil); [self _showAlertInWindow:window withCompletionBlock:completionBlock]; } - (void)_showAlertInWindow:(nullable NSWindow *)window withCompletionBlock:(nullable TVCAlertCompletionBlock)completionBlock { NSAssert((self.alertFinished == NO), @"Cannot show alert because it has already finished"); /* Bring window forward if -showAlert is called more than once */ if (self.alertVisible) { [self.panel makeKeyAndOrderFront:nil]; return; } /* Do not allow changes to be made to the alert */ self.alertImmutable = YES; /* Non-blocking alerts which are created through this initializer should not stay on top of other apps when app is not key. */ if (window == nil) { self.window.hidesOnDeactivate = YES; } /* Perform layout */ [self _layout]; /* Present alert */ self.completionBlock = completionBlock; self.alertVisible = YES; if (window) { LogToConsoleDebug("[%{public}@] Running alert sheet in window: %{public}@", self, window); } else { LogToConsoleDebug("[%{public}@] Running non-blocking alert", self); } if (window) { self.launchedAs = TVCAlertLaunchedAsSheet; [window beginSheet:self.panel completionHandler:^(NSModalResponse returnCode) { [self _alertSheetDidEndWithReturnCode:returnCode]; }]; } else { self.launchedAs = TVCAlertLaunchedAsNonblockingPanel; [self.panel makeKeyAndOrderFront:nil]; } } - (TVCAlertResponseButton)runModal { NSAssert((self.alertFinished == NO), @"Cannot show alert because it has already finished"); /* Do not allow this method to be called while modal is running */ NSAssert((self.alertVisible == NO), @"Cannot show alert because it's already visible"); /* Do not allow changes to be made to the alert */ self.alertImmutable = YES; /* Perform layout */ [self _layout]; /* Present alert */ self.alertVisible = YES; self.launchedAs = TVCAlertLaunchedAsModal; LogToConsoleDebug("[%{public}@] Running modal alert", self); return [NSApp runModalForWindow:self.panel]; } #pragma mark - #pragma mark Layout - (void)_layout { /* Do not perform more than once */ NSAssert((self.layoutPerformed == NO), @"Cannot perform layout multiple times"); /* Context */ self.warningIconImageView.hidden = (self.type != TVCAlertTypeWarning); NSView *contentView = self.panel.contentView; NSTextField *messageTextField = self.messageTextField; NSTextField *informativeTextField = self.informativeTextField; NSView *accessoryView = self.accessoryView; NSButton *suppressionButton = self.suppressionButton; BOOL showsSuppressionButton = self.showsSuppressionButton; NSView *firstButtonAnchor = nil; /* Toggle accessory view */ if (accessoryView) { [contentView addSubview:accessoryView]; [contentView addConstraints: @[ /* Align top of accessory view to bottom of informative text field */ [NSLayoutConstraint constraintWithItem:accessoryView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:informativeTextField attribute:NSLayoutAttributeBottom multiplier:1.0 constant:16.0], /* Align leading of accessory view to leading of message text field */ [NSLayoutConstraint constraintWithItem:accessoryView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:messageTextField attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0], /* Align trailing of accessory view to trailing of content view */ [NSLayoutConstraint constraintWithItem:contentView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:accessoryView attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:20.0] ] ]; firstButtonAnchor = accessoryView; } /* Toggle suppression button */ if (showsSuppressionButton) { NSView *buttonAnchor = ((accessoryView) ?: informativeTextField); [contentView addConstraint: /* Align top of suppression button with top of anchor */ [NSLayoutConstraint constraintWithItem:suppressionButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:buttonAnchor attribute:NSLayoutAttributeBottom multiplier:1.0 constant:16.0] ]; firstButtonAnchor = suppressionButton; } else { [suppressionButton removeFromSuperviewWithoutNeedingDisplay]; } /* Add first button */ NSAssert((self.firstButton.hidden == NO), @"At least one button must be added to alert before presentation"); firstButtonAnchor = ((firstButtonAnchor) ?: informativeTextField); [contentView addConstraint: /* Align top of first button with top of anchor */ [NSLayoutConstraint constraintWithItem:self.firstButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:firstButtonAnchor attribute:NSLayoutAttributeBottom multiplier:1.0 constant:20.0] ]; /* Remove buttons we aren't using */ /* We do this because even when hidden, their constraints still apply to the layout. We could remove the constraints themselves, but this is an easier solution. */ if (self.thirdButton.hidden) { [self.thirdButton removeFromSuperviewWithoutNeedingDisplay]; } if (self.secondButton.hidden) { [self.secondButton removeFromSuperviewWithoutNeedingDisplay]; } /* Update state */ self.layoutPerformed = YES; LogToConsoleDebug("[%{public}@] Layout performed", self); } #pragma mark - #pragma mark Buttons - (void)buttonPressed:(id)sender { NSInteger buttonClicked = [sender tag]; LogToConsoleDebug("[%{public}@] Button pressed: %ld", self, buttonClicked); TVCAlertButtonClickedBlock actionBlock = nil; if (buttonClicked == TVCAlertResponseButtonFirst) { actionBlock = self.firstButtonAction; } else if (buttonClicked == TVCAlertResponseButtonSecond) { actionBlock = self.secondButtonAction; } else if (buttonClicked == TVCAlertResponseButtonThird) { actionBlock = self.thirdButtonAction; } if (actionBlock != nil && actionBlock(self, buttonClicked) == NO) { LogToConsoleDebug("[%{public}@] Button action block denied alert dismissal", self); return; } [self endAlertWithResponse:buttonClicked]; } - (NSArray *)buttons { NSMutableArray *buttons = [[NSMutableArray alloc] initWithCapacity:3]; if (self.firstButton.hidden == NO) { [buttons addObject:self.firstButton]; } if (self.secondButton.hidden == NO) { [buttons addObject:self.secondButton]; } if (self.secondButton.hidden == NO) { [buttons addObject:self.secondButton]; } return [buttons copy]; } - (NSButton *)setTitle:(NSString *)title forButton:(TVCAlertResponseButton)button { switch (button) { case TVCAlertResponseButtonFirst: [self setTitle:title forButtonAtIndex:0]; break; case TVCAlertResponseButtonSecond: [self setTitle:title forButtonAtIndex:1]; break; case TVCAlertResponseButtonThird: [self setTitle:title forButtonAtIndex:2]; break; default: NSAssert(NO, @"Invalid button type"); break; } return nil; } - (NSButton *)setTitle:(NSString *)title forButtonAtIndex:(NSUInteger)index { NSAssert((index >= 0 && index <= 2), @"Index of button is out of bounds. " "Index: %lu, Range: 0 - 2", index); NSButton *button = nil; if (index == 0) { button = self.firstButton; } else if (index == 1) { button = self.secondButton; } else if (index == 2) { button = self.thirdButton; } return [self _setTitle:title forButton:button]; } - (NSButton *)_setTitle:(NSString *)title forButton:(NSButton *)button { NSParameterAssert(title != nil); NSAssert((self.alertImmutable == NO), @"Cannot add button because alert is immutable"); if (button.hidden) { button.hidden = NO; } button.title = title; button.accessibilityTitle = TXTLS(@"Accessibility[wbj-gr]", title); return button; } - (void)setButtonClickedBlock:(nullable TVCAlertButtonClickedBlock)block forButton:(TVCAlertResponseButton)button { switch (button) { case TVCAlertResponseButtonFirst: [self setButtonClickedBlock:block forButtonAtIndex:0]; break; case TVCAlertResponseButtonSecond: [self setButtonClickedBlock:block forButtonAtIndex:1]; break; case TVCAlertResponseButtonThird: [self setButtonClickedBlock:block forButtonAtIndex:2]; break; default: NSAssert(NO, @"Invalid button type"); break; } } - (void)setButtonClickedBlock:(nullable TVCAlertButtonClickedBlock)block forButtonAtIndex:(NSUInteger)index { NSAssert((self.alertFinished == NO), @"Cannot set button clicked block because alert is finished"); NSAssert((index >= 0 && index <= 2), @"Index of button is out of bounds. " "Index: %lu, Range: 0 - 2", index); if (index == 0) { self.firstButtonAction = block; } else if (index == 1) { self.secondButtonAction = block; } else if (index == 2) { self.thirdButtonAction = block; } LogToConsoleDebug("[%{public}@] Setting button action block at index: %lu", self, index); } - (void)endAlert { [self endAlertWithResponse:TVCAlertResponseButtonFirst]; } - (void)endAlertWithResponse:(TVCAlertResponseButton)response { NSAssert((self.alertFinished == NO), @"Cannot end alert because it has already finished"); NSAssert(self.alertVisible, @"Cannot end alert because it isn't visible"); self.alertFinished = YES; switch (self.launchedAs) { case TVCAlertLaunchedAsNonblockingPanel: { [self _postCompletionBlockWithResponse:response]; [self.panel orderOut:nil]; break; } case TVCAlertLaunchedAsSheet: { [NSApp endSheet:self.panel returnCode:response]; break; } case TVCAlertLaunchedAsModal: { [NSApp stopModalWithCode:response]; [self.panel orderOut:nil]; break; } } LogToConsoleDebug("[%{public}@] Alert dismissed", self); self.alertVisible = NO; // Dereference completion block when finished because the // completion block may be the only reference to self. self.completionBlock = nil; } #pragma mark - #pragma mark Setter/Getter - (NSImage *)icon { return self.iconImageView.image; } - (void)setIcon:(nullable NSImage *)icon { NSAssert((self.alertImmutable == NO), @"Cannot change value because alert is immutable"); if (icon == nil) { icon = [NSImage imageNamed:@"NSApplicationIcon"]; } self.iconImageView.image = icon; } - (NSString *)messageText { return self.messageTextField.stringValue; } - (void)setMessageText:(NSString *)messageText { NSParameterAssert(messageText != nil); NSAssert((self.alertImmutable == NO), @"Cannot change value because alert is immutable"); self.messageTextField.stringValue = messageText; } - (NSString *)informativeText { return self.informativeTextField.stringValue; } - (void)setInformativeText:(NSString *)informativeText { NSParameterAssert(informativeText != nil); NSAssert((self.alertImmutable == NO), @"Cannot change value because alert is immutable"); self.informativeTextField.stringValue = informativeText; } - (void)setShowsSuppressionButton:(BOOL)showsSuppressionButton { NSAssert((self.alertImmutable == NO), @"Cannot change value because alert is immutable"); if (self->_showsSuppressionButton != showsSuppressionButton) { self->_showsSuppressionButton = showsSuppressionButton; } } - (void)setAccessoryView:(nullable NSView *)accessoryView { NSAssert((self.alertImmutable == NO), @"Cannot change value because alert is immutable"); if (self->_accessoryView != accessoryView) { self->_accessoryView = accessoryView; } } - (NSWindow *)window { return self.panel; } #pragma mark - #pragma mark Utilities - (void)_postCompletionBlockWithResponse:(TVCAlertResponseButton)response { if (self.completionBlock) { self.completionBlock(self, response); } } #pragma mark - #pragma mark Panel Delegate - (void)_alertSheetDidEndWithReturnCode:(NSInteger)returnCode { [self _postCompletionBlockWithResponse:returnCode]; [self.panel orderOut:nil]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Errors/TVCErrorMessagePopover.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TVCErrorMessagePopoverPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _messageMaximumWidth 330.0 #define _messageHorizontalPadding 5.0 #define _messageVerticalPadding 5.0 #define _errorIconWidth 15.0 #define _errorIconHeight 15.0 #define _errorIconHorizontalPadding 5.0 #define _errorIconVerticalPadding 6.0 @interface TVCErrorMessagePopoverView : NSPopover @end @interface TVCErrorMessagePopover () @property (nonatomic, strong, nullable) NSPopover *popover; @end @implementation TVCErrorMessagePopover - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithMessage:(NSString *)message relativeToView:(NSView *)view { NSParameterAssert(message != nil); NSParameterAssert(view != nil); if ((self = [super init])) { self->_message = [message copy]; self->_view = view; return self; } return nil; } - (void)dealloc { [self close]; } - (void)_createPopover { /* Create view controller */ NSViewController *viewController = [NSViewController new]; /* Create view */ NSView *popoverView = [[NSView alloc] initWithFrame:NSZeroRect]; popoverView.translatesAutoresizingMaskIntoConstraints = NO; viewController.view = popoverView; /* Create image view */ NSImageView *errorIcon = [NSImageView new]; errorIcon.translatesAutoresizingMaskIntoConstraints = NO; errorIcon.editable = NO; errorIcon.image = [NSImage imageNamed:@"ErroneousTextFieldValueIndicator"]; [errorIcon addConstraints: @[ [NSLayoutConstraint constraintWithItem:errorIcon attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:_errorIconWidth], [NSLayoutConstraint constraintWithItem:errorIcon attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:_errorIconHeight] ] ]; [popoverView addSubview:errorIcon]; [popoverView addConstraints: @[ [NSLayoutConstraint constraintWithItem:errorIcon attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:popoverView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:_errorIconHorizontalPadding], [NSLayoutConstraint constraintWithItem:errorIcon attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:popoverView attribute:NSLayoutAttributeTop multiplier:1.0 constant:_errorIconVerticalPadding], ] ]; /* Create message */ NSTextField *errorMessage = [NSTextField new]; errorMessage.translatesAutoresizingMaskIntoConstraints = NO; errorMessage.editable = NO; errorMessage.bordered = NO; errorMessage.drawsBackground = NO; errorMessage.stringValue = self.message; errorMessage.cell.wraps = YES; errorMessage.preferredMaxLayoutWidth = _messageMaximumWidth; /* Add message */ [popoverView addSubview:errorMessage]; [popoverView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:[errorIcon]-messageHorizontalPadding-[errorMessage]-messageHorizontalPadding-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:@{@"messageHorizontalPadding" : @(_messageHorizontalPadding)} views:NSDictionaryOfVariableBindings(errorIcon, errorMessage)]]; [popoverView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-messageVerticalPadding-[errorMessage]-messageVerticalPadding-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:@{@"messageVerticalPadding" : @(_messageVerticalPadding)} views:NSDictionaryOfVariableBindings(errorMessage)]]; /* Create popover */ NSPopover *popover = [TVCErrorMessagePopoverView new]; popover.delegate = (id)self; popover.contentViewController = viewController; popover.behavior = NSPopoverBehaviorTransient; popover.animates = NO; self.popover = popover; } - (void)showRelativeToRect:(NSRect)rect { [self showRelativeToRect:rect preferredEdge:NSRectEdgeMaxY]; } - (void)showRelativeToRect:(NSRect)rect preferredEdge:(NSRectEdge)preferredEdge { if (self.popover == nil) { [self _createPopover]; } [self.popover showRelativeToRect:rect ofView:self.view preferredEdge:preferredEdge]; } - (void)close { NSPopover *popover = self.popover; if (popover == nil) { return; } [popover close]; self.popover = nil; } - (void)popoverWillShow:(NSNotification *)notification { if ([self.delegate respondsToSelector:@selector(errorMessagePopoverWillShow:)]) { [self.delegate errorMessagePopoverWillShow:self]; } } - (void)popoverDidShow:(NSNotification *)notification { if ([self.delegate respondsToSelector:@selector(errorMessagePopoverDidShow:)]) { [self.delegate errorMessagePopoverDidShow:self]; } } - (void)popoverWillClose:(NSNotification *)notification { if ([self.delegate respondsToSelector:@selector(errorMessagePopoverWillClose:)]) { [self.delegate errorMessagePopoverWillClose:self]; } } - (void)popoverDidClose:(NSNotification *)notification { if ([self.delegate respondsToSelector:@selector(errorMessagePopoverDidClose:)]) { [self.delegate errorMessagePopoverDidClose:self]; } } @end #pragma mark - @implementation TVCErrorMessagePopoverView - (void)mouseDown:(NSEvent *)event { [self close]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Errors/TVCErrorMessagePopoverController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TVCErrorMessagePopoverPrivate.h" #import "TVCErrorMessagePopoverControllerPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCErrorMessagePopoverController () @property (nonatomic, strong, nullable) TVCErrorMessagePopover *visiblePopover; @end @implementation TVCErrorMessagePopoverController #pragma mark - #pragma mark Public + (TVCErrorMessagePopoverController *)sharedController { static TVCErrorMessagePopoverController *sharedSelf = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedSelf = [self new]; }); return sharedSelf; } - (void)showMessage:(NSString *)message forView:(NSView *)view { NSParameterAssert(message != nil); NSParameterAssert(view != nil); TVCErrorMessagePopover *popover = self.visiblePopover; BOOL popoverIsSame = (popover && popover.view == view && [popover.message isEqualToString:message]); if (popoverIsSame == NO) { /* Close message already on screen being tracked */ if (popover) { [popover close]; } /* Show message */ popover = [[TVCErrorMessagePopover alloc] initWithMessage:message relativeToView:view]; } [popover showRelativeToRect:view.bounds]; if (popoverIsSame == NO) { self.visiblePopover = popover; } } - (void)closeMessage { [self _closePopoverForView:nil]; } - (void)closeMessageForView:(NSView *)view { [self _closePopoverForView:view]; } #pragma mark - #pragma mark Private - (void)_closePopoverForView:(nullable NSView *)view { TVCErrorMessagePopover *popover = self.visiblePopover; if (popover == nil || (view && view != popover.view)) { return; } [popover close]; self.visiblePopover = nil; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Input Text Field/TVCAutoExpandingTextField.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCAutoExpandingTextField.h" NS_ASSUME_NONNULL_BEGIN @implementation TVCAutoExpandingTextField - (NSSize)intrinsicContentSize { if (self.cell.wraps == NO) { return super.intrinsicContentSize; } NSRect originalFrame = self.frame; originalFrame.size.height = CGFLOAT_MAX; NSSize newFrameSize = [self.cell cellSizeForBounds:originalFrame]; return newFrameSize; } - (void)textDidChange:(NSNotification *)notification { [super textDidChange:notification]; [self invalidateIntrinsicContentSize]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Input Text Field/TVCAutoExpandingTokenField.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCAutoExpandingTokenField.h" NS_ASSUME_NONNULL_BEGIN @implementation TVCAutoExpandingTokenField - (NSSize)intrinsicContentSize { if (self.cell.wraps == NO) { return super.intrinsicContentSize; } NSRect originalFrame = self.frame; originalFrame.size.height = CGFLOAT_MAX; NSSize newFrameSize = [self.cell cellSizeForBounds:originalFrame]; return newFrameSize; } - (void)textDidChange:(NSNotification *)notification { [super textDidChange:notification]; [self invalidateIntrinsicContentSize]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Input Text Field/TVCTextFormatterMenu.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSColorHelper.h" #import "IRCColorFormat.h" #import "TLOLocalization.h" #import "TVCTextViewWithIRCFormatterPrivate.h" #import "TVCTextFormatterMenuPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _formattingMenuRainbowColorMenuItemTag 299 #define _formattingMenuHexColorMenuItemTag 300 @interface TVCTextViewIRCFormattingMenu () @property (readonly, nullable) TVCTextViewWithIRCFormatter *textField; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *formatterMenu; @property (nonatomic, weak, readwrite) IBOutlet NSMenu *foregroundColorMenu; @property (nonatomic, weak, readwrite) IBOutlet NSMenu *backgroundColorMenu; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *foregroundColorSetMenuItem; @property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *backgroundColorSetMenuItem; - (IBAction)emptyAction:(id)sender; @end @implementation TVCTextViewIRCFormattingMenu #pragma mark - #pragma mark Menu Management - (void)awakeFromNib { [super awakeFromNib]; [self generateColorList]; } - (nullable TVCTextViewWithIRCFormatter *)textField { id firstResponder = [[NSApp keyWindow] firstResponder]; if ([firstResponder isKindOfClass:[TVCTextViewWithIRCFormatter class]]) { return firstResponder; } return nil; } - (BOOL)validateMenuItem:(NSMenuItem *)item { if (self.textField == nil) { return NO; } switch (item.tag) { case 100: // Bold { BOOL boldText = self.textIsBold; item.state = boldText; if (boldText) { item.action = @selector(removeBoldCharFromTextBox:); } else { item.action = @selector(insertBoldCharIntoTextBox:); } return YES; } case 101: // Italics { BOOL italicText = self.textIsItalicized; item.state = italicText; if (italicText) { item.action = @selector(removeItalicCharFromTextBox:); } else { item.action = @selector(insertItalicCharIntoTextBox:); } return YES; } case 102: // Monospace { BOOL monospaceText = self.textIsMonospace; item.state = monospaceText; if (monospaceText) { item.action = @selector(removeMonospaceCharFromTextBox:); } else { item.action = @selector(insertMonospaceCharIntoTextBox:); } return YES; } case 103: // Spoiler { BOOL spoilerText = self.textHasSpoiler; item.state = spoilerText; if (spoilerText) { item.action = @selector(removeSpoilerCharFromTextBox:); } else { item.action = @selector(insertSpoilerCharIntoTextBox:); } return YES; } case 104: // Strikethrough { BOOL struckthroughText = self.textIsStruckthrough; item.state = struckthroughText; if (struckthroughText) { item.action = @selector(removeStrikethroughCharFromTextBox:); } else { item.action = @selector(insertStrikethroughCharIntoTextBox:); } return YES; } case 105: // Underline { BOOL underlineText = self.textIsUnderlined; item.state = underlineText; if (underlineText) { item.action = @selector(removeUnderlineCharFromTextBox:); } else { item.action = @selector(insertUnderlineCharIntoTextBox:); } return YES; } case 108: // Foreground Color Missing { item.hidden = self.textHasForegroundColor; return YES; } case 107: // Foreground Color Set { item.hidden = (self.textHasForegroundColor == NO); /* Do not enable menu item when there is spoiler */ return (self.textHasSpoiler == NO); } case 110: // Background Color Missing { item.hidden = self.textHasBackgroundColor; /* Require foreground color before background color can be set */ return self.textHasForegroundColor; } case 109: // Background Color Set { item.hidden = (self.textHasBackgroundColor == NO); return (self.textHasSpoiler == NO); } default: { break; } } return YES; } - (void)emptyAction:(id)sender { /* Empty action used to validate submenus */ } #pragma mark - #pragma mark Menu Generation - (void)generateColorList { /* While we could technically load this from a file; we don't need to. That just adds extra space to the app when we already need to have an array of colors in the binary. */ NSColorList *colorList = [[NSColorList alloc] initWithName:TXTLS(@"iwp-cg")]; [[NSColor formatterColors] enumerateObjectsUsingBlock:^(NSColor *color, NSUInteger index, BOOL *stop) { [colorList setColor:color forKey:TXTLS(@"ham-vk", index)]; }]; [[NSColorPanel sharedColorPanel] attachColorList:colorList]; } #pragma mark - #pragma mark Formatting Properties - (BOOL)propertyIsSet:(IRCTextFormatterEffectType)formatterEffect { NSRange selectedTextRange = self.textField.selectedRange; return [self.textField.attributedString IRCFormatterAttributeSetInRange:formatterEffect range:selectedTextRange]; } - (BOOL)textIsBold { return [self propertyIsSet:IRCTextFormatterEffectBold]; } - (BOOL)textIsItalicized { return [self propertyIsSet:IRCTextFormatterEffectItalic]; } - (BOOL)textIsMonospace { return [self propertyIsSet:IRCTextFormatterEffectMonospace]; } - (BOOL)textIsStruckthrough { return [self propertyIsSet:IRCTextFormatterEffectStrikethrough]; } - (BOOL)textIsUnderlined { return [self propertyIsSet:IRCTextFormatterEffectUnderline]; } - (BOOL)textHasForegroundColor { return [self propertyIsSet:IRCTextFormatterEffectForegroundColor]; } - (BOOL)textHasBackgroundColor { return [self propertyIsSet:IRCTextFormatterEffectBackgroundColor]; } - (BOOL)textHasSpoiler { return [self propertyIsSet:IRCTextFormatterEffectSpoiler]; } #pragma mark - #pragma mark Formatting Storage Helpers - (void)applyEffectToTextBox:(IRCTextFormatterEffectType)formatterEffect withValue:(id)value inRange:(NSRange)limitRange { NSMutableAttributedString *stringMutableCopy = [self mutableStringAtRange:limitRange]; if (stringMutableCopy == nil) { return; } [self applyEffect:formatterEffect withValue:value toMutableString:stringMutableCopy]; [self applyAttributedStringToTextBox:stringMutableCopy inRange:limitRange]; if (value == nil && (formatterEffect == IRCTextFormatterEffectForegroundColor || formatterEffect == IRCTextFormatterEffectSpoiler)) { [self.textField resetFontColorInRange:limitRange]; } if (formatterEffect == IRCTextFormatterEffectMonospace && value == nil) { [self.textField resetFontInRange:limitRange]; } } - (nullable NSMutableAttributedString *)mutableStringAtRange:(NSRange)limitRange { if (limitRange.location == NSNotFound || limitRange.length == 0) { return nil; } NSAttributedString *stringSubstring = [self.textField.attributedString attributedSubstringFromRange:limitRange]; return [stringSubstring mutableCopy]; } - (void)applyEffect:(IRCTextFormatterEffectType)formatterEffect withValue:(id)value toMutableString:(NSMutableAttributedString *)mutableString { [self applyEffect:formatterEffect withValue:value inRange:mutableString.range toMutableString:mutableString]; } - (void)applyEffect:(IRCTextFormatterEffectType)formatterEffect withValue:(id)value inRange:(NSRange)limitRange toMutableString:(NSMutableAttributedString *)mutableString { if (value) { [mutableString setIRCFormatterAttribute:formatterEffect value:value range:limitRange]; } else { [mutableString removeIRCFormatterAttribute:formatterEffect range:limitRange]; } } - (void)applyAttributedStringToTextBox:(NSMutableAttributedString *)mutableString inRange:(NSRange)limitRange { if ([self.textField shouldChangeTextInRange:limitRange replacementString:mutableString.string] == NO) { return; } [self.textField.textStorage replaceCharactersInRange:limitRange withAttributedString:mutableString]; [self.textField didChangeText]; [self.textField setSelectedRange:limitRange]; } #pragma mark - #pragma mark Add Formatting - (void)insertBoldCharIntoTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectBold withValue:@(YES) inRange:selectedTextRange]; } - (void)insertItalicCharIntoTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectItalic withValue:@(YES) inRange:selectedTextRange]; } - (void)insertMonospaceCharIntoTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectMonospace withValue:@(YES) inRange:selectedTextRange]; } - (void)insertStrikethroughCharIntoTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectStrikethrough withValue:@(YES) inRange:selectedTextRange]; } - (void)insertUnderlineCharIntoTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectUnderline withValue:@(YES) inRange:selectedTextRange]; } - (void)insertForegroundColorCharIntoTextBox:(id)sender { if ([sender tag] == _formattingMenuRainbowColorMenuItemTag) { [self insertRainbowColorCharInfoTextBox:sender asForegroundColor:YES]; return; } else if ([sender tag] == _formattingMenuHexColorMenuItemTag) { NSColorPanel *colorPanel = [NSColorPanel sharedColorPanel]; [colorPanel setTarget:self]; [colorPanel setAction:@selector(foregroundColorPanelColorChanged:)]; [colorPanel setAlphaValue:1.0]; [colorPanel setMode:NSColorPanelModeColorList]; [colorPanel setColor:[NSColor formatterWhiteColor]]; [colorPanel orderFront:nil]; } NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectForegroundColor withValue:@([sender tag]) inRange:selectedTextRange]; } - (void)insertBackgroundColorCharIntoTextBox:(id)sender { if ([sender tag] == _formattingMenuRainbowColorMenuItemTag) { [self insertRainbowColorCharInfoTextBox:sender asForegroundColor:NO]; return; } else if ([sender tag] == _formattingMenuHexColorMenuItemTag) { NSColorPanel *colorPanel = [NSColorPanel sharedColorPanel]; [colorPanel setTarget:self]; [colorPanel setAction:@selector(backgroundColorPanelColorChanged:)]; [colorPanel setAlphaValue:1.0]; [colorPanel setMode:NSColorPanelModeColorList]; [colorPanel setColor:[NSColor formatterBlackColor]]; [colorPanel orderFront:nil]; } NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectBackgroundColor withValue:@([sender tag]) inRange:selectedTextRange]; } - (void)insertRainbowColorCharInfoTextBox:(id)sender asForegroundColor:(BOOL)asForegroundColor { NSRange selectedTextRange = self.textField.selectedRange; NSMutableAttributedString *mutableStringCopy = [self mutableStringAtRange:selectedTextRange]; if (mutableStringCopy == nil) { return; } [mutableStringCopy beginEditing]; NSUInteger rainbowArrayIndex = 0; NSArray *colorCodes = @[@(4), @(7), @(8), @(3), @(12), @(2), @(6)]; for (NSUInteger charCountIndex = 0; charCountIndex < mutableStringCopy.length; charCountIndex++) { if (rainbowArrayIndex > 6) { rainbowArrayIndex = 0; } NSUInteger currentColorCode = [colorCodes unsignedIntegerAtIndex:rainbowArrayIndex]; NSRange currentCharacterRange = NSMakeRange(charCountIndex, 1); if (asForegroundColor) { [self applyEffect:IRCTextFormatterEffectForegroundColor withValue:@(currentColorCode) inRange:currentCharacterRange toMutableString:mutableStringCopy]; } else { [self applyEffect:IRCTextFormatterEffectBackgroundColor withValue:@(currentColorCode) inRange:currentCharacterRange toMutableString:mutableStringCopy]; } rainbowArrayIndex += 1; } [mutableStringCopy endEditing]; [self applyAttributedStringToTextBox:mutableStringCopy inRange:selectedTextRange]; } - (void)foregroundColorPanelColorChanged:(NSColorPanel *)sender { NSRange selectedTextRange = self.textField.selectedRange; NSColor *color = sender.color; NSUInteger colorDigit = [[NSColor formatterColors] indexOfObject:color]; if (colorDigit == NSNotFound) { [self applyEffectToTextBox:IRCTextFormatterEffectForegroundColor withValue:color inRange:selectedTextRange]; } else { [self applyEffectToTextBox:IRCTextFormatterEffectForegroundColor withValue:@(colorDigit) inRange:selectedTextRange]; } } - (void)backgroundColorPanelColorChanged:(NSColorPanel *)sender { NSRange selectedTextRange = self.textField.selectedRange; NSColor *color = sender.color; NSUInteger colorDigit = [[NSColor formatterColors] indexOfObject:color]; if (colorDigit == NSNotFound) { [self applyEffectToTextBox:IRCTextFormatterEffectBackgroundColor withValue:color inRange:selectedTextRange]; } else { [self applyEffectToTextBox:IRCTextFormatterEffectBackgroundColor withValue:@(colorDigit) inRange:selectedTextRange]; } } - (void)insertSpoilerCharIntoTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectSpoiler withValue:@(YES) inRange:selectedTextRange]; [self applyEffectToTextBox:IRCTextFormatterEffectForegroundColor withValue:@(14) inRange:selectedTextRange]; [self applyEffectToTextBox:IRCTextFormatterEffectBackgroundColor withValue:@(14) inRange:selectedTextRange]; } #pragma mark - #pragma mark Remove Formatting - (void)removeBoldCharFromTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectBold withValue:nil inRange:selectedTextRange]; } - (void)removeItalicCharFromTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectItalic withValue:nil inRange:selectedTextRange]; } - (void)removeMonospaceCharFromTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectMonospace withValue:nil inRange:selectedTextRange]; } - (void)removeStrikethroughCharFromTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectStrikethrough withValue:nil inRange:selectedTextRange]; } - (void)removeUnderlineCharFromTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectUnderline withValue:nil inRange:selectedTextRange]; } - (void)removeForegroundColorCharFromTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectForegroundColor withValue:nil inRange:selectedTextRange]; } - (void)removeBackgroundColorCharFromTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectBackgroundColor withValue:nil inRange:selectedTextRange]; } - (void)removeSpoilerCharFromTextBox:(id)sender { NSRange selectedTextRange = self.textField.selectedRange; [self applyEffectToTextBox:IRCTextFormatterEffectForegroundColor withValue:nil inRange:selectedTextRange]; [self applyEffectToTextBox:IRCTextFormatterEffectBackgroundColor withValue:nil inRange:selectedTextRange]; [self applyEffectToTextBox:IRCTextFormatterEffectSpoiler withValue:nil inRange:selectedTextRange]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Input Text Field/TVCTextViewWithIRCFormatter.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSStringHelper.h" #import "IRCColorFormat.h" #import "TPCPreferencesLocal.h" #import "TVCTextViewWithIRCFormatterPrivate.h" NS_ASSUME_NONNULL_BEGIN #define TVCTextViewWithIRCFormatterWidthPadding 1.0 #define TVCTextViewWithIRCFormatterHeightPadding 2.0 @interface TVCTextViewWithIRCFormatter () @property (nonatomic, strong) TLOKeyEventHandler *keyEventHandler; @end @implementation TVCTextViewWithIRCFormatter - (nullable instancetype)initWithCoder:(NSCoder *)coder { if ((self = [super initWithCoder:coder])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.delegate = (id)self; if ([TPCPreferences rightToLeftFormatting]) { self.baseWritingDirection = NSWritingDirectionRightToLeft; } else { self.baseWritingDirection = NSWritingDirectionLeftToRight; } self.textContainerInset = NSMakeSize(TVCTextViewWithIRCFormatterWidthPadding, TVCTextViewWithIRCFormatterHeightPadding); self.keyEventHandler = [[TLOKeyEventHandler alloc] initWithTarget:self]; // The following serve as defaults and are supposed to be replaced self.preferredFont = [NSFont systemFontOfSize:[NSFont systemFontSize]]; self.preferredFontColor = [NSColor textColor]; } - (void)mouseDown:(NSEvent *)theEvent { [self.window makeFirstResponder:self]; [super mouseDown:theEvent]; } #pragma mark - #pragma mark Keyboard Shortcuts - (void)setKeyHandlerTarget:(id)target { [self.keyEventHandler setKeyHandlerTarget:target]; } - (void)registerSelector:(SEL)selector key:(NSUInteger)keyCode modifiers:(NSUInteger)modifiers { [self.keyEventHandler registerSelector:selector key:keyCode modifiers:modifiers]; } - (void)registerSelector:(SEL)selector character:(UniChar)character modifiers:(NSUInteger)modifiers { [self.keyEventHandler registerSelector:selector character:character modifiers:modifiers]; } - (void)registerSelector:(SEL)selector characters:(NSRange)characterRange modifiers:(NSUInteger)modifiers { [self.keyEventHandler registerSelector:selector characters:characterRange modifiers:modifiers]; } - (BOOL)performedCustomKeyboardEvent:(NSEvent *)e { if ([self.keyEventHandler processKeyEvent:e]) { return YES; } return NO; } - (void)keyDownToSuper:(NSEvent *)e { [super keyDown:e]; } #pragma mark - #pragma mark Value Management - (NSArray *)readablePasteboardTypes { return @[NSPasteboardTypeString, NSFilenamesPboardType]; } - (NSArray *)acceptableDragTypes { return @[NSPasteboardTypeString, NSFilenamesPboardType]; } - (NSString *)stringValue { return [self.string copy]; } - (NSString *)stringValueWithIRCFormatting { return self.attributedString.stringFormattedForIRC; } - (void)setStringValue:(NSString *)stringValue { NSParameterAssert(stringValue != nil); [self.textStorage replaceCharactersInRange:self.range withString:stringValue]; [self didChangeText]; } - (NSAttributedString *)attributedStringValue { return [[self attributedString] copy]; } - (void)setAttributedStringValue:(NSAttributedString *)attributedStringValue { NSParameterAssert(attributedStringValue != nil); [self.undoManager removeAllActions]; [self.textStorage replaceCharactersInRange:self.range withAttributedString:attributedStringValue]; [self didChangeText]; } - (void)setStringValueWithIRCFormatting:(NSString *)stringValueWithIRCFormatting { NSParameterAssert(stringValueWithIRCFormatting != nil); NSAttributedString *formattedValue = [stringValueWithIRCFormatting attributedStringWithIRCFormatting:self.preferredFont preferredFontColor:self.preferredFontColor honorFormattingPreference:NO]; if (formattedValue) { self.attributedStringValue = formattedValue; } } #pragma mark - - (void)textDidChange:(NSNotification *)aNotification { if (self.stringLength < 1) { [self resetTypeSetterAttributes]; } } #pragma mark - - (void)updateAllFontSizesToMatchTheDefaultFont { CGFloat newPointSize = self.preferredFont.pointSize; [self.textStorage beginEditing]; [self.textStorage enumerateAttribute:NSFontAttributeName inRange:self.range options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { if (fabs([value pointSize]) == fabs(newPointSize)) { return; } NSFont *font = [RZFontManager() convertFont:value toSize:newPointSize]; if (font) { [self.textStorage removeAttribute:NSFontAttributeName range:range]; [self.textStorage addAttribute:NSFontAttributeName value:font range:range]; } }]; [self.textStorage endEditing]; } - (void)setPreferredFont:(NSFont *)preferredFont { NSParameterAssert(preferredFont != nil); if (self->_preferredFont != preferredFont) { self->_preferredFont = [preferredFont copy]; [self modifyTypingAttributes:@{ NSFontAttributeName : self->_preferredFont }]; } } - (void)setPreferredFontColor:(NSColor *)preferredFontColor { NSParameterAssert(preferredFontColor != nil); if (self->_preferredFontColor != preferredFontColor) { self->_preferredFontColor = [preferredFontColor copy]; [self modifyTypingAttributes:@{ NSForegroundColorAttributeName : self->_preferredFontColor }]; self.insertionPointColor = self->_preferredFontColor; } } - (void)resetTypeSetterAttributes { self.typingAttributes = @{ NSFontAttributeName : self.preferredFont, NSForegroundColorAttributeName : self.preferredFontColor }; } - (void)modifyTypingAttributes:(NSDictionary *)typingAttributes { NSMutableDictionary *typingAttributesMutable = [self.typingAttributes mutableCopy]; [typingAttributesMutable addEntriesFromDictionary:typingAttributes]; self.typingAttributes = typingAttributesMutable; } - (void)resetFontInRange:(NSRange)range { NSDictionary *newAttributes = @{ NSFontAttributeName : self.preferredFont }; [self.textStorage addAttributes:newAttributes range:range]; } - (void)resetFontColorInRange:(NSRange)range { NSDictionary *newAttributes = @{ NSForegroundColorAttributeName : self.preferredFontColor }; [self.textStorage addAttributes:newAttributes range:range]; } #pragma mark - #pragma mark Line Counting - (NSRect)selectedRect { NSLayoutManager *layoutManager = self.layoutManager; NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:self.selectedRange actualCharacterRange:NULL]; NSRect boundingRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:self.textContainer]; NSPoint containerOrigin = [self textContainerOrigin]; return NSInsetRect(boundingRect, containerOrigin.x, containerOrigin.y); } - (TVCTextViewCaretLocation)caretLocation { NSUInteger stringLength = self.stringLength; if (stringLength == 0) { return TVCTextViewCaretLocationOnlyLine; } NSRange selectedRange = self.selectedRange; NSLayoutManager *layoutManager = self.layoutManager; /* Check first line */ BOOL inFirstLine = (selectedRange.location == 0); if (inFirstLine == NO) { NSRange firstLineRange; [layoutManager lineFragmentRectForGlyphAtIndex:0 effectiveRange:&firstLineRange]; inFirstLine = (selectedRange.location <= NSMaxRange(firstLineRange)); } /* Check last line */ BOOL inLastLine = (NSMaxRange(selectedRange) == stringLength); if (inLastLine == NO) { NSRange lastLineRange; [layoutManager lineFragmentRectForGlyphAtIndex:(stringLength - 1) effectiveRange:&lastLineRange]; inLastLine = (selectedRange.location >= lastLineRange.location); } /* Process results */ if (inFirstLine && inLastLine) { return TVCTextViewCaretLocationOnlyLine; } else if (inFirstLine) { return TVCTextViewCaretLocationFirstLine; } else if (inLastLine) { return TVCTextViewCaretLocationLastLine; } return TVCTextViewCaretLocationMiddle; } - (CGFloat)highestHeightBelowHeight:(CGFloat)maximumHeight withPadding:(CGFloat)valuePadding { NSLayoutManager *layoutManager = self.layoutManager; BOOL skipLastFragmentCheck = NO; NSUInteger numberOfGlyphs = layoutManager.numberOfGlyphs; NSUInteger totalLineHeight = valuePadding; for (NSUInteger i = 0; i < numberOfGlyphs; i++) { NSRange lineRange; NSRect rect = [layoutManager lineFragmentRectForGlyphAtIndex:i effectiveRange:&lineRange]; if ((totalLineHeight + rect.size.height) <= maximumHeight) { totalLineHeight += rect.size.height; } else { skipLastFragmentCheck = YES; break; } i = NSMaxRange(lineRange); } if (skipLastFragmentCheck) { return totalLineHeight; } NSRect lastFragmentRect = layoutManager.extraLineFragmentRect; if ((totalLineHeight + lastFragmentRect.size.height) <= maximumHeight) { totalLineHeight += lastFragmentRect.size.height; } return totalLineHeight; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Input Text Field/TVCValidatedComboBox.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOLocalization.h" #import "TVCErrorMessagePopoverControllerPrivate.h" #import "TVCValidatedComboBox.h" NS_ASSUME_NONNULL_BEGIN @interface TVCValidatedComboBox () /* Maintain cached value so that the drawing does not call the validation block every time that it is called. */ @property (nonatomic, assign) BOOL cachedValidValue; @property (nonatomic, assign, readwrite) BOOL valueIsPredefined; @property (nonatomic, assign) BOOL validationPerformed; @property (nonatomic, assign) BOOL listVisible; @property (nonatomic, assign) BOOL selectionChangedWhileListVisible; @property (nonatomic, assign) BOOL selectionChangedWhileSetting; @property (nonatomic, copy, nullable, readwrite) NSString *lastValidationErrorDescription; @end @interface TVCValidatedComboBoxCell () @property (readonly) NSColor *erroneousValueBackgroundColor; @property (readonly) BOOL parentValueIsValid; @property (readonly) TVCValidatedComboBox *parentField; @end @implementation TVCValidatedComboBox #pragma mark - #pragma mark Public API (Combo Box Text Field) - (void)awakeFromNib { [super awakeFromNib]; self.cachedValidValue = NO; [RZNotificationCenter() addObserver:self selector:@selector(comboBoxSelectionDidChange:) name:NSComboBoxSelectionDidChangeNotification object:self]; [RZNotificationCenter() addObserver:self selector:@selector(comboBoxWillPopUp:) name:NSComboBoxWillPopUpNotification object:self]; [RZNotificationCenter() addObserver:self selector:@selector(comboBoxWillDismiss:) name:NSComboBoxWillDismissNotification object:self]; } - (void)dealloc { [self closeValidationErrorPopover]; [RZNotificationCenter() removeObserver:self]; } - (BOOL)drawsBackground { return NO; } - (NSString *)value { NSString *stringValue = self.objectValueOfSelectedItem; if (stringValue) { return stringValue; } stringValue = self.stringValue; if (self.stringValueUsesOnlyFirstToken) { stringValue = stringValue.trimAndGetFirstToken; } else if (self.stringValueIsTrimmed) { stringValue = stringValue.trim; } if (stringValue.length == 0) { if ( self.defaultValue && self.stringValueIsInvalidOnEmpty == NO) { return self.defaultValue; } } return stringValue; } - (NSString *)lowercaseValue { return self.value.lowercaseString; } - (NSString *)uppercaseValue { return self.value.uppercaseString; } - (NSInteger)integerValue { return self.value.integerValue; } - (void)setIntegerValue:(NSInteger)integerValue { self.stringValue = [NSString stringWithInteger:integerValue]; } - (BOOL)valueIsEmpty { return (self.stringValue.length == 0); } - (BOOL)valueIsValid { return self.cachedValidValue; } #pragma mark - #pragma mark Interval Validation - (void)comboBoxSelectionDidChange:(NSNotification *)notification { self.selectionChangedWhileListVisible = self.listVisible; if (self.selectionChangedWhileSetting) { self.selectionChangedWhileSetting = NO; [self recalculateSelection]; } } - (void)comboBoxWillPopUp:(NSNotification *)notification { self.listVisible = YES; } - (void)comboBoxWillDismiss:(NSNotification *)notification { self.listVisible = NO; if (self.selectionChangedWhileListVisible) { self.selectionChangedWhileListVisible = NO; [self recalculateSelection]; } } - (void)textDidChange:(NSNotification *)notification { /* NSComboBoxCell observes NSTextDidChangeNotification to know when to update the selection index. */ /* Our observance of this notification is competing with theirs which causes a race condition. To work around this, we wait until the next pass of the main loop to perform our validation because the internals of the cell should have selection index up to date by then to reflect the string value. */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [self recalculateSelection]; }); } - (void)setStringValue:(NSString *)stringValue { super.stringValue = stringValue; NSUInteger objectIndex = [self indexOfItemWithObjectValue:stringValue]; if (objectIndex != NSNotFound) { self.selectionChangedWhileSetting = YES; [self selectItemAtIndex:objectIndex]; return; } [self resetSelection]; } - (void)resetSelection { self.valueIsPredefined = NO; [self _valueChangedAction]; } - (void)recalculateSelection { self.valueIsPredefined = (self.indexOfSelectedItem >= 0); [self _valueChangedAction]; } - (void)_valueChangedAction { if (self.valueIsPredefined) { /* If predefined selection, then the value is valid no matter what because it is a value WE defined. */ self.cachedValidValue = YES; self.lastValidationErrorDescription = nil; } else { [self performValidation]; } [self _valueChangedActionPostflight]; } - (void)_valueChangedActionPostflight { [self closeValidationErrorPopover]; [self informCallbackTextDidChange]; [self setNeedsDisplay:YES]; } - (void)informCallbackTextDidChange { if (self.textDidChangeCallback == nil) { return; } if ([self.textDidChangeCallback respondsToSelector:@selector(validatedTextFieldTextDidChange:)]) { [self.textDidChangeCallback performSelector:@selector(validatedTextFieldTextDidChange:) withObject:self]; } } - (void)performValidation { NSString *stringToValidate = self.stringValue; NSString *errorDescription = nil; if (stringToValidate.length > 0) { if (self.validationBlock) { errorDescription = self.validationBlock(stringToValidate); } } else { if (self.performValidationWhenEmpty) { errorDescription = self.validationBlock(stringToValidate); } else if (self.stringValueIsInvalidOnEmpty) { errorDescription = TXTLS(@"BasicLanguage[fo8-1h]"); } } self.cachedValidValue = (errorDescription == nil); self.lastValidationErrorDescription = errorDescription; self.validationPerformed = YES; } - (BOOL)showValidationErrorPopover { if (self.validationPerformed == NO) { [self performValidation]; } NSString *errorDescription = self.lastValidationErrorDescription; if (errorDescription == nil) { return NO; } if (self.window == nil) { return NO; } [[TVCErrorMessagePopoverController sharedController] showMessage:errorDescription forView:self]; return YES; } - (void)closeValidationErrorPopover { [[TVCErrorMessagePopoverController sharedController] closeMessageForView:self]; } - (void)viewWillMoveToWindow:(nullable NSWindow *)newWindow { /* While outside logic is responsible for displaying the validation error message, it would not hurt to help it a little bit by dismissing the message when the view is no longer on a window. */ if (newWindow == nil) { [self closeValidationErrorPopover]; } } @end #pragma mark - #pragma mark Text Field Cell @implementation TVCValidatedComboBoxCell - (NSColor *)erroneousValueBackgroundColor { return [NSColor colorWithCalibratedRed:1.0 green:0.0 blue:0.0 alpha:0.05]; } - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { if (self.parentValueIsValid == NO) { NSRect backgroundFrame = cellFrame; backgroundFrame.origin.x += 1.0; backgroundFrame.origin.y += 1.0; backgroundFrame.size.width -= 2.0; backgroundFrame.size.height -= 2.0; NSColor *backgroundColor = self.erroneousValueBackgroundColor; NSBezierPath *backgroundFill = [NSBezierPath bezierPathWithRect:backgroundFrame]; [backgroundColor set]; [backgroundFill fill]; } [super drawInteriorWithFrame:cellFrame inView:controlView]; } - (TVCValidatedComboBox *)parentField { return (TVCValidatedComboBox *)self.controlView; } - (BOOL)parentValueIsValid { return self.parentField.valueIsValid; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Input Text Field/TVCValidatedTextField.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOLocalization.h" #import "TVCErrorMessagePopoverControllerPrivate.h" #import "TVCValidatedTextField.h" NS_ASSUME_NONNULL_BEGIN @interface TVCValidatedTextField () /* Maintain cached value so that the drawing does not call the validation block every time that it is called. */ @property (nonatomic, assign) BOOL cachedValidValue; @property (nonatomic, assign) BOOL validationPerformed; @property (nonatomic, copy, nullable, readwrite) NSString *lastValidationErrorDescription; @end @interface TVCValidatedTextFieldCell () @property (readonly) NSColor *erroneousValueBackgroundColor; @property (readonly) BOOL parentValueIsValid; @property (readonly) TVCValidatedTextField *parentField; @end @implementation TVCValidatedTextField #pragma mark - #pragma mark Public API (Normal Text Field) - (void)awakeFromNib { [super awakeFromNib]; self.cachedValidValue = NO; } - (void)dealloc { [self closeValidationErrorPopover]; } - (BOOL)drawsBackground { return NO; } - (NSString *)value { NSString *stringValue = self.stringValue; if (self.stringValueUsesOnlyFirstToken) { stringValue = stringValue.trimAndGetFirstToken; } else if (self.stringValueIsTrimmed) { stringValue = stringValue.trim; } if (stringValue.length == 0) { if ( self.defaultValue && self.stringValueIsInvalidOnEmpty == NO) { return self.defaultValue; } } return stringValue; } - (NSString *)lowercaseValue { return self.value.lowercaseString; } - (NSString *)uppercaseValue { return self.value.uppercaseString; } - (NSInteger)integerValue { return self.value.integerValue; } - (void)setIntegerValue:(NSInteger)integerValue { self.stringValue = [NSString stringWithInteger:integerValue]; } - (BOOL)valueIsEmpty { return (self.stringValue.length == 0); } - (BOOL)valueIsValid { return self.cachedValidValue; } #pragma mark - #pragma mark Interval Validation - (void)textDidChange:(NSNotification *)notification { [self _valueChangedAction]; } - (void)setStringValue:(NSString *)stringValue { super.stringValue = stringValue; [self _valueChangedAction]; } - (void)_valueChangedAction { [self performValidation]; [self _valueChangedActionPostflight]; } - (void)_valueChangedActionPostflight { [self closeValidationErrorPopover]; [self informCallbackTextDidChange]; } - (void)informCallbackTextDidChange { if (self.textDidChangeCallback == nil) { return; } if ([self.textDidChangeCallback respondsToSelector:@selector(validatedTextFieldTextDidChange:)]) { [self.textDidChangeCallback performSelector:@selector(validatedTextFieldTextDidChange:) withObject:self]; } } - (void)performValidation { NSString *stringToValidate = self.stringValue; NSString *errorDescription = nil; if (stringToValidate.length > 0) { if (self.validationBlock) { errorDescription = self.validationBlock(stringToValidate); } } else { if (self.performValidationWhenEmpty) { errorDescription = self.validationBlock(stringToValidate); } else if (self.stringValueIsInvalidOnEmpty) { errorDescription = TXTLS(@"BasicLanguage[fo8-1h]"); } } self.cachedValidValue = (errorDescription == nil); self.lastValidationErrorDescription = errorDescription; self.validationPerformed = YES; } - (BOOL)showValidationErrorPopover { if (self.validationPerformed == NO) { [self performValidation]; } NSString *errorDescription = self.lastValidationErrorDescription; if (errorDescription == nil) { return NO; } if (self.window == nil) { return NO; } [[TVCErrorMessagePopoverController sharedController] showMessage:errorDescription forView:self]; return YES; } - (void)closeValidationErrorPopover { [[TVCErrorMessagePopoverController sharedController] closeMessageForView:self]; } - (void)viewWillMoveToWindow:(nullable NSWindow *)newWindow { /* While outside logic is responsible for displaying the validation error message, it would not hurt to help it a little bit by dismissing the message when the view is no longer on a window. */ if (newWindow == nil) { [self closeValidationErrorPopover]; } } @end #pragma mark - #pragma mark Text Field Cell @implementation TVCValidatedTextFieldCell - (NSColor *)erroneousValueBackgroundColor { return [NSColor colorWithCalibratedRed:1.0 green:0.0 blue:0.0 alpha:0.05]; } - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { if (self.parentValueIsValid == NO) { NSRect backgroundFrame = cellFrame; backgroundFrame.origin.x += 1.0; backgroundFrame.origin.y += 1.0; backgroundFrame.size.width -= 2.0; backgroundFrame.size.height -= 2.0; NSColor *backgroundColor = self.erroneousValueBackgroundColor; NSBezierPath *backgroundFill = [NSBezierPath bezierPathWithRect:backgroundFrame]; [backgroundColor set]; [backgroundFill fill]; } [super drawInteriorWithFrame:cellFrame inView:controlView]; } - (TVCValidatedTextField *)parentField { return (TVCValidatedTextField *)self.controlView; } - (BOOL)parentValueIsValid { return self.parentField.valueIsValid; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindow.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSStringHelper.h" #import "IRCChannelPrivate.h" #import "IRCChannelMode.h" #import "IRCClientConfig.h" #import "IRCClientPrivate.h" #import "IRCTreeItemPrivate.h" #import "IRCUserRelationsPrivate.h" #import "IRCWorldPrivate.h" #import "TVCDockIconPrivate.h" #import "TVCLogControllerPrivate.h" #import "TVCLogViewPrivate.h" #import "TVCMainWindowAppearancePrivate.h" #import "TVCMainWindowChannelViewPrivate.h" #import "TVCMainWindowLoadingScreenPrivate.h" #import "TVCMainWindowSplitViewPrivate.h" #import "TVCMainWindowTextViewPrivate.h" #import "TVCMainWindowTitlebarAccessoryViewPrivate.h" #import "TVCServerListPrivate.h" #import "TVCServerListAppearancePrivate.h" #import "TVCServerListCellPrivate.h" #import "TVCMemberListPrivate.h" #import "TVCTextFormatterMenuPrivate.h" #import "TVCTextViewWithIRCFormatterPrivate.h" #import "TPCApplicationInfo.h" #import "TPCPreferencesLocal.h" #import "TPCPreferencesUserDefaults.h" #import "TPCThemeControllerPrivate.h" #import "TPCTheme.h" #import "TXGlobalModels.h" #import "TXMasterControllerPrivate.h" #import "TXMenuControllerPrivate.h" #import "THOPluginDispatcherPrivate.h" #import "TLOEncryptionManagerPrivate.h" #import "TLOKeyEventHandler.h" #import "TLOInputHistoryPrivate.h" #import "TLOLocalization.h" #import "TLOLicenseManagerPrivate.h" #import "TLONicknameCompletionStatusPrivate.h" #import "TLONotificationControllerPrivate.h" #import "TLOSpeechSynthesizerPrivate.h" #import "TDCLicenseManagerDialogPrivate.h" #import "TVCMainWindowPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TVCMainWindowAppearanceChangedNotification = @"TVCMainWindowAppearanceChangedNotification"; NSString * const TVCMainWindowRedrawSubviewsNotification = @"TVCMainWindowRedrawSubviewsNotification"; NSString * const TVCMainWindowWillReloadThemeNotification = @"TVCMainWindowWillReloadThemeNotification"; NSString * const TVCMainWindowDidReloadThemeNotification = @"TVCMainWindowDidReloadThemeNotification"; NSString * const TVCMainWindowSelectionChangedNotification = @"TVCMainWindowSelectionChangedNotification"; @interface TVCMainWindow () @property (nonatomic, weak, readwrite) IBOutlet TVCMainWindowChannelView *channelView; @property (nonatomic, weak, readwrite) IBOutlet TVCMainWindowTitlebarAccessoryView *titlebarAccessoryView; @property (nonatomic, weak, readwrite) IBOutlet TVCMainWindowTitlebarAccessoryViewController *titlebarAccessoryViewController; @property (nonatomic, weak, readwrite) IBOutlet TVCMainWindowTitlebarAccessoryViewLockButton *titlebarAccessoryViewLockButton; @property (nonatomic, strong, readwrite) IBOutlet TXMenuControllerMainWindowProxy *mainMenuProxy; @property (nonatomic, strong, readwrite) IBOutlet TVCTextViewIRCFormattingMenu *formattingMenu; @property (nonatomic, unsafe_unretained, readwrite) IBOutlet TVCMainWindowTextView *inputTextField; @property (nonatomic, weak, readwrite) IBOutlet TVCMainWindowSplitView *contentSplitView; @property (nonatomic, weak, readwrite) IBOutlet TVCMainWindowLoadingScreenView *loadingScreen; @property (nonatomic, weak, readwrite) IBOutlet TVCMemberList *memberList; @property (nonatomic, weak, readwrite) IBOutlet TVCServerList *serverList; @property (nonatomic, strong) TLOInputHistory *inputHistoryManager; @property (nonatomic, strong) TLONicknameCompletionStatus *nicknameCompletionStatus; @property (nonatomic, strong, readwrite) TVCMainWindowAppearance *userInterfaceObjects; @property (nonatomic, readwrite, copy) NSArray *selectedItems; @property (nonatomic, readwrite, strong, nullable) IRCTreeItem *selectedItem; @property (nonatomic, copy, nullable) NSArray *previousSelectedItemsId; @property (nonatomic, copy, nullable) NSString *previousSelectedItemId; @property (nonatomic, assign) NSTimeInterval lastKeyWindowStateChange; @property (nonatomic, assign) BOOL lastKeyWindowRedrawFailedBecauseOfOcclusion; @property (nonatomic, strong) TLOKeyEventHandler *keyEventHandler; @property (nonatomic, copy, nullable) NSValue *cachedSwipeOriginPoint; @property (nonatomic, assign, readwrite) double textSizeMultiplier; @property (nonatomic, assign, readwrite) BOOL reloadingTheme; @end #define _treeDragItemType TVCServerListDragType #define _treeDragItemTypes [NSArray arrayWithObject:_treeDragItemType] @implementation TVCMainWindow #pragma mark - #pragma mark Awakening - (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag { if ((self = [super initWithContentRect:contentRect styleMask:style backing:bufferingType defer:flag])) { [self prepareInitialState]; } return self; } - (void)prepareInitialState { self.inputHistoryManager = [[TLOInputHistory alloc] initWithWindow:self]; self.keyEventHandler = [[TLOKeyEventHandler alloc] initWithTarget:self]; self.nicknameCompletionStatus = [[TLONicknameCompletionStatus alloc] initWithWindow:self]; self.previousSelectedItemsId = @[]; self.selectedItems = @[]; self.textSizeMultiplier = 1.0; } - (void)awakeFromNib { [super awakeFromNib]; /* -awakeFromNib is called multiple times because of reloads */ static BOOL _awakeFromNibCalled = NO; if (_awakeFromNibCalled == NO) { _awakeFromNibCalled = YES; [self _awakeFromNib]; } } - (void)_awakeFromNib { self.delegate = (id)self; self.allowsConcurrentViewDrawing = NO; self.alphaValue = [TPCPreferences mainWindowTransparency]; [self addAccessoryViewsToTitlebar]; [self updateAppearance]; [self reloadLoadingScreen]; [self makeMainWindow]; [self makeKeyAndOrderFront:nil]; [self loadWindowState]; [self updateChannelViewArrangement]; [masterController() applicationWakeStepOne]; [themeController() load]; [menuController() prepareInitialState]; [self registerKeyHandlers]; [worldController() setupConfiguration]; [self setupTrees]; [TVCDockIcon drawWithoutCount]; [self observeNotifications]; [masterController() applicationWakeStepTwo]; } - (void)observeNotifications { #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 [RZNotificationCenter() addObserver:self selector:@selector(licenseManagerActivatedLicense:) name:TDCLicenseManagerActivatedLicenseNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(licenseManagerDeactivatedLicense:) name:TDCLicenseManagerDeactivatedLicenseNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(licenseManagerTrialExpired:) name:TDCLicenseManagerTrialExpiredNotification object:nil]; #endif [RZNotificationCenter() addObserver:self selector:@selector(applicationAppearanceChanged:) name:TXApplicationAppearanceChangedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(systemAppearanceChanged:) name:TXSystemAppearanceChangedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(themeVarietyChanged:) name:TPCThemeAppearanceChangedNotification object:nil]; [RZNotificationCenter() addObserver:self selector:@selector(themeVarietyChanged:) name:TPCThemeVarietyChangedNotification object:nil]; } - (void)maybeToggleFullscreenAfterLaunch { BOOL isFullscreen = [RZUserDefaults() boolForKey:@"Window -> Main Window Is Fullscreen'd"]; if (isFullscreen == NO) { return; } [self performSelectorInCommonModes:@selector(toggleFullscreenAfterLaunch) withObject:nil afterDelay:1.0]; } - (void)toggleFullscreenAfterLaunch { if (self.inFullscreenMode) { return; } [self toggleFullScreen:nil]; } - (void)themeVarietyChanged:(NSNotification *)notification { [self reloadTheme]; } - (void)applicationAppearanceChanged:(NSNotification *)notification { [self updateAppearance]; } - (void)systemAppearanceChanged:(NSNotification *)notification { [self notifySystemAppearanceChanged]; } - (BOOL)isUsingDarkAppearance { return self.userInterfaceObjects.isDarkAppearance; } - (void)updateAppearance { TVCMainWindowAppearance *appearance = [[TVCMainWindowAppearance alloc] initWithWindow:self]; self.userInterfaceObjects = appearance; [self updateVibrancyWithAppearance:appearance]; [self notifyApplicationAppearanceChanged]; } - (void)updateVibrancyWithAppearance:(TVCMainWindowAppearance *)appearance { NSParameterAssert(appearance != nil); NSAppearance *appKitAppearance = nil; if (appearance.appKitAppearanceTarget == TXAppKitAppearanceTargetWindow) { appKitAppearance = appearance.appKitAppearance; } self.appearance = appKitAppearance; } - (void)notifyApplicationAppearanceChanged { [super notifyApplicationAppearanceChanged]; [RZNotificationCenter() postNotificationName:TVCMainWindowAppearanceChangedNotification object:self]; } - (void)updateAlphaValueToReflectPreferences { [self updateAlphaValueToReflectPreferencesAnimated:NO]; } - (void)updateAlphaValueToReflectPreferencesAnimated:(BOOL)animate { if (self.inFullscreenMode) { return; } double alphaValue = [TPCPreferences mainWindowTransparency]; if (animate) { [self animator].alphaValue = alphaValue; } else { self.alphaValue = alphaValue; } } - (void)loadWindowState { [self restoreWindowStateUsingKeyword:@"Main Window"]; [self restoreSavedContentSplitViewState]; } - (void)saveWindowState { [RZUserDefaults() setBool:self.isInFullscreenMode forKey:@"Window -> Main Window Is Fullscreen'd"]; [self saveWindowStateUsingKeyword:@"Main Window"]; [self saveContentSplitViewState]; [self saveSelection]; } - (void)prepareForApplicationTermination { LogToConsoleTerminationProgress("Removing main window observers"); [RZNotificationCenter() removeObserver:self]; LogToConsoleTerminationProgress("Saving window state"); [self saveWindowState]; LogToConsoleTerminationProgress("Giving up server list & member list delegation"); self.serverList.dataSource = nil; self.serverList.delegate = nil; self.serverList.keyDelegate = nil; self.memberList.keyDelegate = nil; [self.memberList assignToChannel:nil]; self.delegate = (id)self; self.selectedItems = nil; self.selectedItem = nil; LogToConsoleTerminationProgress("Closing main window"); [self close]; } #pragma mark - #pragma mark Item Update - (void)reloadMainWindowFrameOnScreenChange { if (masterController().applicationIsTerminating) { return; } [TVCDockIcon resetCachedCount]; [TVCDockIcon updateDockIcon]; [self updateAppearance]; } - (void)resetSelectedItemState { if (masterController().applicationIsTerminating) { return; } id selectedItem = self.selectedItem; if (selectedItem) { [selectedItem resetState]; } [TVCDockIcon updateDockIcon]; } - (void)reloadSubviewDrawings { [RZNotificationCenter() postNotificationName:TVCMainWindowRedrawSubviewsNotification object:self]; } - (void)reloadViewControllerDrawings { if (masterController().applicationIsTerminating) { return; } for (IRCTreeItem *item in self.selectedItems) { [item.viewController.backingView redrawViewIfNeeded]; } } #pragma mark - #pragma mark NSWindow Delegate - (void)windowDidDeminiaturize:(NSNotification *)notification { // [self reloadViewControllerDrawings]; } - (void)windowDidChangeScreen:(NSNotification *)notification { [self reloadMainWindowFrameOnScreenChange]; } - (void)windowDidChangeOcclusionState:(NSNotification *)notification { if (self.occluded) { return; } if (self.lastKeyWindowRedrawFailedBecauseOfOcclusion) { self.lastKeyWindowRedrawFailedBecauseOfOcclusion = NO; [self reloadSubviewDrawings]; } else { /* We keep track of the last subview redraw so that we do not draw too often. Current maximum is 1.0 second. */ NSTimeInterval timeDifference = ([NSDate timeIntervalSince1970] - self.lastKeyWindowStateChange); if (timeDifference > 1.0) { [self reloadSubviewDrawings]; } } } - (void)windowDidBecomeKey:(NSNotification *)notification { self.lastKeyWindowStateChange = [NSDate timeIntervalSince1970]; [self resetSelectedItemState]; if (self.occluded) { self.lastKeyWindowRedrawFailedBecauseOfOcclusion = YES; return; } [self reloadSubviewDrawings]; // [self reloadViewControllerDrawings]; } - (void)windowDidResignKey:(NSNotification *)notification { self.lastKeyWindowStateChange = [NSDate timeIntervalSince1970]; [self reloadSubviewDrawings]; } - (BOOL)window:(NSWindow *)window shouldPopUpDocumentPathMenu:(NSMenu *)menu { return NO; } - (BOOL)window:(NSWindow *)window shouldDragDocumentWithEvent:(NSEvent *)event from:(NSPoint)dragImageLocation withPasteboard:(NSPasteboard *)pasteboard { return NO; } - (void)windowDidResize:(NSNotification *)notification { [self.inputTextField recalculateTextViewSize]; } - (BOOL)windowShouldZoom:(NSWindow *)awindow toFrame:(NSRect)newFrame { return (self.inFullscreenMode == NO); } - (NSSize)window:(NSWindow *)window willUseFullScreenContentSize:(NSSize)proposedSize { return proposedSize; } - (NSApplicationPresentationOptions)window:(NSWindow *)window willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions { return proposedOptions; } - (void)windowDidExitFullScreen:(NSNotification *)notification { [self updateAlphaValueToReflectPreferencesAnimated:YES]; } - (void)windowWillEnterFullScreen:(NSNotification *)notification { [self animator].alphaValue = 1.0; } - (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)client { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMenu *editorMenu = self.inputTextField.menu; NSMenuItem *formatterMenu = self.formattingMenu.formatterMenu; NSInteger formatterMenuIndex = [editorMenu indexOfItemWithTitle:formatterMenu.title]; if (formatterMenuIndex < 0) { [editorMenu addItem:[NSMenuItem separatorItem]]; [editorMenu addItem:formatterMenu]; } self.inputTextField.menu = editorMenu; }); return self.inputTextField; } #pragma mark - #pragma mark Keyboard Shortcuts - (void)setKeyHandlerTarget:(id)target { [self.keyEventHandler setKeyHandlerTarget:target]; } - (void)registerSelector:(SEL)selector key:(NSUInteger)keyCode modifiers:(NSUInteger)modifiers { [self.keyEventHandler registerSelector:selector key:keyCode modifiers:modifiers]; } - (void)registerSelector:(SEL)selector character:(UniChar)character modifiers:(NSUInteger)modifiers { [self.keyEventHandler registerSelector:selector character:character modifiers:modifiers]; } - (void)registerInputSelector:(SEL)selector key:(NSUInteger)keyCode modifiers:(NSUInteger)modifiers { [self.inputTextField registerSelector:selector key:keyCode modifiers:modifiers]; } - (void)registerInputSelector:(SEL)selector character:(UniChar)character modifiers:(NSUInteger)modifiers { [self.inputTextField registerSelector:selector character:character modifiers:modifiers]; } - (BOOL)performedCustomKeyboardEvent:(NSEvent *)e { if ([self.keyEventHandler processKeyEvent:e]) { return YES; } return NO; } - (void)redirectKeyDown:(NSEvent *)e { [self.inputTextField focus]; if (e.keyCode == TXKeyEnterCode || e.keyCode == TXKeyReturnCode) { return; } [self.inputTextField keyDown:e]; } - (void)memberListKeyDown:(NSEvent *)e { [self redirectKeyDown:e]; } - (void)serverListKeyDown:(NSEvent *)e { [self redirectKeyDown:e]; } - (void)registerKeyHandlers { [self.inputTextField setKeyHandlerTarget:self]; /* Window keyboard shortcuts */ [self registerSelector:@selector(exitFullscreenMode:) key:TXKeyEscapeCode modifiers:0]; [self registerSelector:@selector(tab:) key:TXKeyTabCode modifiers:0]; [self registerSelector:@selector(shiftTab:) key:TXKeyTabCode modifiers:NSEventModifierFlagShift]; [self registerSelector:@selector(selectPreviousSelection:) key:TXKeyTabCode modifiers:NSEventModifierFlagOption]; [self registerSelector:@selector(textFormattingBold:) character:'b' modifiers:NSEventModifierFlagCommand]; [self registerSelector:@selector(textFormattingUnderline:) character:'u' modifiers:(NSEventModifierFlagControl | NSEventModifierFlagShift)]; [self registerSelector:@selector(textFormattingItalic:) character:'i' modifiers:(NSEventModifierFlagControl | NSEventModifierFlagShift)]; [self registerSelector:@selector(textFormattingForegroundColor:) character:'c' modifiers:(NSEventModifierFlagControl | NSEventModifierFlagShift)]; [self registerSelector:@selector(textFormattingBackgroundColor:) character:'h' modifiers:(NSEventModifierFlagControl | NSEventModifierFlagShift)]; [self registerSelector:@selector(speakPendingNotifications:) character:'.' modifiers:NSEventModifierFlagCommand]; [self registerSelector:@selector(inputHistoryUp:) character:'p' modifiers:NSEventModifierFlagControl]; [self registerSelector:@selector(inputHistoryDown:) character:'n' modifiers:NSEventModifierFlagControl]; /* Text field keyboard shortcuts */ [self registerInputSelector:@selector(sendControlEnterMessageMaybe:) key:TXKeyEnterCode modifiers:NSEventModifierFlagControl]; [self registerInputSelector:@selector(sendMessageAsAction:) key:TXKeyReturnCode modifiers:NSEventModifierFlagCommand]; [self registerInputSelector:@selector(sendMessageAsAction:) key:TXKeyEnterCode modifiers:NSEventModifierFlagCommand]; [self registerInputSelector:@selector(focusWebview:) character:'l' modifiers:(NSEventModifierFlagOption | NSEventModifierFlagCommand)]; [self registerInputSelector:@selector(inputHistoryUpWithScrollCheck:) key:TXKeyUpArrowCode modifiers:0]; [self registerInputSelector:@selector(inputHistoryUpWithScrollCheck:) key:TXKeyUpArrowCode modifiers:NSEventModifierFlagOption]; [self registerInputSelector:@selector(inputHistoryDownWithScrollCheck:) key:TXKeyDownArrowCode modifiers:0]; [self registerInputSelector:@selector(inputHistoryDownWithScrollCheck:) key:TXKeyDownArrowCode modifiers:NSEventModifierFlagOption]; } #pragma mark - #pragma mark Navigation - (void)navigateServerListEntries:(nullable NSArray *)scannedRows entryCount:(NSInteger)entryCount startingPoint:(NSInteger)startingPoint isMovingDown:(BOOL)isMovingDown navigationType:(TVCServerListNavigationMovementType)navigationType selectionType:(TVCServerListNavigationSelectionType)selectionType { NSParameterAssert(entryCount > 0); NSParameterAssert(startingPoint >= 0); NSInteger currentPosition = startingPoint; while (1) { /* Move to next selection */ if (isMovingDown) { currentPosition += 1; } else { currentPosition -= 1; } /* Make sure selection is within our bounds */ if (currentPosition >= entryCount || currentPosition < 0) { if (isMovingDown == NO && currentPosition < 0) { currentPosition = (entryCount - 1); } else { currentPosition = 0; } } if (currentPosition == startingPoint) { break; } /* Get next selection depending on data source */ id item; if (scannedRows) { item = scannedRows[currentPosition]; } else { item = [self.serverList itemAtRow:currentPosition]; } /* Skip entries depending on navigation type */ if (selectionType == TVCServerListNavigationSelectionTypeChannel) { if ([item isChannel] == NO && [item isPrivateMessage] == NO) { continue; } } else if (selectionType == TVCServerListNavigationSelectionTypeServer) { if ([item isClient] == NO) { continue; } } /* Select current item if it is matched by our condition */ if (navigationType == TVCServerListNavigationMovementTypeAll) { [self select:item]; break; } else if (navigationType == TVCServerListNavigationMovementTypeActive) { if ([item isActive]) { [self select:item]; break; } } else if (navigationType == TVCServerListNavigationMovementTypeUnread) { if ([item isUnread]) { [self select:item]; break; } } } } - (void)navigateChannelEntries:(BOOL)isMovingDown withNavigationType:(TVCServerListNavigationMovementType)navigationType { if ([TPCPreferences channelNavigationIsServerSpecific]) { [self navigateChannelEntriesWithinServerScope:isMovingDown withNavigationType:navigationType]; } else { [self navigateChannelEntriesOutsideServerScope:isMovingDown withNavigationType:navigationType]; } } - (void)navigateChannelEntriesOutsideServerScope:(BOOL)isMovingDown withNavigationType:(TVCServerListNavigationMovementType)navigationType { NSInteger entryCount = self.serverList.numberOfRows; NSInteger startingPoint = [self.serverList rowForItem:self.selectedItem]; [self navigateServerListEntries:nil entryCount:entryCount startingPoint:startingPoint isMovingDown:isMovingDown navigationType:navigationType selectionType:TVCServerListNavigationSelectionTypeChannel]; } - (void)navigateChannelEntriesWithinServerScope:(BOOL)isMovingDown withNavigationType:(TVCServerListNavigationMovementType)navigationType { NSArray *scannedRows = [self.serverList itemsFromParentGroup:self.selectedItem]; /* We add selected server so navigation falls within its scope if its the selected item */ scannedRows = [scannedRows arrayByAddingObject:self.selectedClient]; [self navigateServerListEntries:scannedRows entryCount:scannedRows.count startingPoint:[scannedRows indexOfObject:self.selectedItem] isMovingDown:isMovingDown navigationType:navigationType selectionType:TVCServerListNavigationSelectionTypeChannel]; } - (void)navigateServerEntries:(BOOL)isMovingDown withNavigationType:(TVCServerListNavigationMovementType)navigationType { NSArray *scannedRows = self.serverList.groupItems; [self navigateServerListEntries:scannedRows entryCount:scannedRows.count startingPoint:[scannedRows indexOfObject:self.selectedClient] isMovingDown:isMovingDown navigationType:navigationType selectionType:TVCServerListNavigationSelectionTypeServer]; } - (void)navigateToNextEntry:(BOOL)isMovingDown { NSInteger entryCount = self.serverList.numberOfRows; NSInteger startingPoint = [self.serverList rowForItem:self.selectedItem]; [self navigateServerListEntries:nil entryCount:entryCount startingPoint:startingPoint isMovingDown:isMovingDown navigationType:TVCServerListNavigationMovementTypeAll selectionType:TVCServerListNavigationSelectionTypeAny]; } - (void)selectPreviousChannel:(NSEvent *)e { [self navigateChannelEntries:NO withNavigationType:TVCServerListNavigationMovementTypeAll]; } - (void)selectNextChannel:(NSEvent *)e { [self navigateChannelEntries:YES withNavigationType:TVCServerListNavigationMovementTypeAll]; } - (void)selectPreviousUnreadChannel:(NSEvent *)e { [self navigateChannelEntries:NO withNavigationType:TVCServerListNavigationMovementTypeUnread]; } - (void)selectNextUnreadChannel:(NSEvent *)e { [self navigateChannelEntries:YES withNavigationType:TVCServerListNavigationMovementTypeUnread]; } - (void)selectPreviousActiveChannel:(NSEvent *)e { [self navigateChannelEntries:NO withNavigationType:TVCServerListNavigationMovementTypeActive]; } - (void)selectNextActiveChannel:(NSEvent *)e { [self navigateChannelEntries:YES withNavigationType:TVCServerListNavigationMovementTypeActive]; } - (void)selectPreviousServer:(NSEvent *)e { [self navigateServerEntries:NO withNavigationType:TVCServerListNavigationMovementTypeAll]; } - (void)selectNextServer:(NSEvent *)e { [self navigateServerEntries:YES withNavigationType:TVCServerListNavigationMovementTypeAll]; } - (void)selectPreviousActiveServer:(NSEvent *)e { [self navigateServerEntries:NO withNavigationType:TVCServerListNavigationMovementTypeActive]; } - (void)selectNextActiveServer:(NSEvent *)e { [self navigateServerEntries:YES withNavigationType:TVCServerListNavigationMovementTypeActive]; } - (void)selectPreviousSelection:(NSEvent *)e { [self selectPreviousItem]; } - (void)selectNextWindow:(NSEvent *)e { [self navigateToNextEntry:YES]; } - (void)selectPreviousWindow:(NSEvent *)e { [self navigateToNextEntry:NO]; } #pragma mark - #pragma mark View Controls - (void)changeTextSize:(BOOL)bigger { #define MinimumZoomMultiplier 0.5 #define MaximumZoomMultiplier 3.0 #define ZoomMultiplierRatio 1.2 double textSizeMultiplier = self.textSizeMultiplier; if (bigger) { textSizeMultiplier *= ZoomMultiplierRatio; if (textSizeMultiplier > MaximumZoomMultiplier) { return; } self.textSizeMultiplier = textSizeMultiplier; } else { textSizeMultiplier /= ZoomMultiplierRatio; if (textSizeMultiplier < MinimumZoomMultiplier) { return; } self.textSizeMultiplier = textSizeMultiplier; } for (IRCClient *u in worldController().clientList) { [u.viewController changeTextSize:bigger]; for (IRCChannel *c in u.channelList) { [c.viewController changeTextSize:bigger]; } } #undef MinimumZoomMultiplier #undef MaximumZoomMultiplier #undef ZoomMultiplierRatio } - (void)markAllAsRead { [self markAllAsReadInGroup:nil]; } - (void)markAllAsReadInGroup:(nullable IRCTreeItem *)item { BOOL markScrollback = [TPCPreferences autoAddScrollbackMark]; for (IRCClient *u in worldController().clientList) { if (markScrollback) { [u.viewController mark]; } for (IRCChannel *c in u.channelList) { if (markScrollback) { [c.viewController mark]; } [c resetState]; } } [TVCDockIcon updateDockIcon]; if (item) { [self reloadTreeGroup:item]; } else { [self reloadTree]; } } - (void)reloadTheme { if (self.reloadingTheme == NO) { self.reloadingTheme = YES; } else { return; } [RZNotificationCenter() postNotificationName:TVCMainWindowWillReloadThemeNotification object:self]; XRPerformBlockAsynchronouslyOnMainQueue(^{ if (masterController().applicationIsTerminating) { return; } [TVCLogView emptyCaches]; [self _reloadTheme_performReload]; }); } - (void)_reloadTheme_performReload { for (IRCClient *u in worldController().clientList) { [u.viewController reloadTheme]; for (IRCChannel *c in u.channelList) { [c.viewController reloadTheme]; } } self.reloadingTheme = NO; [RZNotificationCenter() postNotificationName:TVCMainWindowDidReloadThemeNotification object:self]; } - (void)clearContentsOfClient:(IRCClient *)client { NSParameterAssert(client != nil); [client resetState]; [client.viewController clear]; [self reloadTreeItem:client]; } - (void)clearContentsOfChannel:(IRCChannel *)channel { NSParameterAssert(channel != nil); [channel resetState]; [channel.viewController clear]; [self reloadTreeItem:channel]; } - (void)clearAllViews { for (IRCClient *u in worldController().clientList) { [self clearContentsOfClient:u]; for (IRCChannel *c in u.channelList) { [self clearContentsOfChannel:c]; } } [self markAllAsRead]; } #pragma mark - #pragma mark Actions - (void)completeNickname:(BOOL)movingForward { [self.nicknameCompletionStatus completeNickname:movingForward]; } - (void)tab:(NSEvent *)e { TXTabKeyAction tabKeyAction = [TPCPreferences tabKeyAction]; if (tabKeyAction == TXTabKeyActionNicknameComplete) { [self completeNickname:YES]; } else if (tabKeyAction == TXTabKeyActionUnreadChannel) { [self navigateChannelEntries:YES withNavigationType:TVCServerListNavigationMovementTypeUnread]; } } - (void)shiftTab:(NSEvent *)e { TXTabKeyAction tabKeyAction = [TPCPreferences tabKeyAction]; if (tabKeyAction == TXTabKeyActionNicknameComplete) { [self completeNickname:NO]; } else if (tabKeyAction == TXTabKeyActionUnreadChannel) { [self navigateChannelEntries:NO withNavigationType:TVCServerListNavigationMovementTypeUnread]; } } - (void)sendControlEnterMessageMaybe:(NSEvent *)e { if ([TPCPreferences controlEnterSendsMessage]) { [self textEntered]; return; } [self.inputTextField keyDownToSuper:e]; } - (void)sendMessageAsAction:(NSEvent *)e { if ([TPCPreferences commandReturnSendsMessageAsAction]) { [self inputTextAsCommand:IRCRemoteCommandPrivmsgAction]; return; } [self textEntered]; } - (void)moveInputHistory:(BOOL)movingUp checkScroller:(BOOL)checkScroller event:(NSEvent *)event { if (checkScroller) { TVCTextViewCaretLocation caretLocation = self.inputTextField.caretLocation; if (caretLocation != TVCTextViewCaretLocationOnlyLine) { BOOL atTop = (caretLocation == TVCTextViewCaretLocationFirstLine); BOOL atBottom = (caretLocation == TVCTextViewCaretLocationLastLine); if ((atTop && event.keyCode == TXKeyDownArrowCode) || (atBottom && event.keyCode == TXKeyUpArrowCode) || (atTop == NO && atBottom == NO)) { [self.inputTextField keyDownToSuper:event]; return; } } } NSAttributedString *stringValue = self.inputTextField.attributedStringValue; if (movingUp) { stringValue = [self.inputHistoryManager up:stringValue]; } else { stringValue = [self.inputHistoryManager down:stringValue]; } if (stringValue == nil) { return; } self.inputTextField.attributedStringValue = stringValue; [self.inputTextField focus]; if (movingUp == NO) { self.inputTextField.selectedRange = NSMakeRange(0, 0); } } - (void)inputHistoryUp:(NSEvent *)e { [self moveInputHistory:YES checkScroller:NO event:e]; } - (void)inputHistoryDown:(NSEvent *)e { [self moveInputHistory:NO checkScroller:NO event:e]; } - (void)inputHistoryUpWithScrollCheck:(NSEvent *)e { [self moveInputHistory:YES checkScroller:YES event:e]; } - (void)inputHistoryDownWithScrollCheck:(NSEvent *)e { [self moveInputHistory:NO checkScroller:YES event:e]; } - (void)textFormattingBold:(NSEvent *)e { if (self.formattingMenu.textIsBold) { [self.formattingMenu removeBoldCharFromTextBox:nil]; } else { [self.formattingMenu insertBoldCharIntoTextBox:nil]; } } - (void)textFormattingItalic:(NSEvent *)e { if (self.formattingMenu.textIsItalicized) { [self.formattingMenu removeItalicCharFromTextBox:nil]; } else { [self.formattingMenu insertItalicCharIntoTextBox:nil]; } } - (void)textFormattingStrikethrough:(NSEvent *)e { if (self.formattingMenu.textIsStruckthrough) { [self.formattingMenu removeStrikethroughCharFromTextBox:nil]; } else { [self.formattingMenu insertStrikethroughCharIntoTextBox:nil]; } } - (void)textFormattingUnderline:(NSEvent *)e { if (self.formattingMenu.textIsUnderlined) { [self.formattingMenu removeUnderlineCharFromTextBox:nil]; } else { [self.formattingMenu insertUnderlineCharIntoTextBox:nil]; } } - (void)textFormattingForegroundColor:(NSEvent *)e { if (self.formattingMenu.textHasSpoiler) { return; } if (self.formattingMenu.textHasForegroundColor) { [self.formattingMenu removeForegroundColorCharFromTextBox:nil]; return; } NSRect textFieldFrame = self.inputTextField.frame; textFieldFrame.origin.y -= 200; textFieldFrame.origin.x += 100; [self.formattingMenu.foregroundColorMenu popUpMenuPositioningItem:nil atLocation:textFieldFrame.origin inView:self.inputTextField]; } - (void)textFormattingBackgroundColor:(NSEvent *)e { if (self.formattingMenu.textHasSpoiler) { return; } if (self.formattingMenu.textHasForegroundColor == NO) { return; } if (self.formattingMenu.textHasBackgroundColor) { [self.formattingMenu removeForegroundColorCharFromTextBox:nil]; return; } NSRect textFieldFrame = self.inputTextField.frame; textFieldFrame.origin.y -= 200; textFieldFrame.origin.x += 100; [self.formattingMenu.backgroundColorMenu popUpMenuPositioningItem:nil atLocation:textFieldFrame.origin inView:self.inputTextField]; } - (void)exitFullscreenMode:(NSEvent *)e // escape key { if (self.inFullscreenMode) { [self toggleFullScreen:nil]; return; } [self.inputTextField keyDown:e]; } - (void)speakPendingNotifications:(NSEvent *)e { [[TXSharedApplication sharedSpeechSynthesizer] stopSpeakingAndMoveForward]; } - (void)focusWebview:(NSEvent *)e { if (self.attachedSheet != nil) { return; } TVCLogController *viewController = self.selectedViewController; if (viewController == nil) { return; } NSView *webView = viewController.backingView.webView; [self makeFirstResponder:webView]; } #pragma mark - #pragma mark Utilities - (void)textEntered { [self inputTextAsCommand:IRCRemoteCommandPrivmsg]; } - (void)inputTextAsCommand:(IRCRemoteCommand)command { [self.nicknameCompletionStatus clear]; NSAttributedString *stringValue = self.inputTextField.attributedStringValue; if (stringValue.length == 0) { return; } self.inputTextField.attributedStringValue = [NSAttributedString attributedString]; [self.inputHistoryManager add:stringValue]; [self inputText:stringValue asCommand:command]; } - (void)inputText:(id)string asCommand:(IRCRemoteCommand)command { NSParameterAssert(string != nil); if (self.selectedItem == nil) { return; } NSString *stringValue = [THOPluginDispatcher interceptUserInput:string command:command]; if (stringValue == nil) { return; } [self.selectedClient inputText:stringValue asCommand:command]; } #pragma mark - #pragma mark Swipe Events /* Three Finger Swipe Event This event will only work if System Settings -> Trackpad -> More Gestures -> Swipe between full-screen apps is not set to "Swipe left or right with three fingers" */ - (void)swipeWithEvent:(NSEvent *)event { CGFloat x = event.deltaX; BOOL invertedScrollingDirection = [RZUserDefaults() boolForKey:@"com.apple.swipescrolldirection"]; if (invertedScrollingDirection) { x = (x * (-1)); } if (x > 0) { [self selectNextWindow:nil]; } else if (x < 0) { [self selectPreviousWindow:nil]; } } - (void)beginGestureWithEvent:(NSEvent *)event { CGFloat swipeMinimumLength = [TPCPreferences swipeMinimumLength]; if (swipeMinimumLength < 1.0) { return; } NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:nil]; if (touches.count != 2) { return; } NSArray *touchArray = touches.allObjects; self.cachedSwipeOriginPoint = [self touchesToPoint:touchArray[0] fingerB:touchArray[1]]; } - (NSValue *)touchesToPoint:(NSTouch *)fingerA fingerB:(NSTouch *)fingerB { NSParameterAssert(fingerA != nil); NSParameterAssert(fingerB != nil); NSSize deviceSize = fingerA.deviceSize; CGFloat x = ((fingerA.normalizedPosition.x + fingerB.normalizedPosition.x) / 2.0 * deviceSize.width); CGFloat y = ((fingerA.normalizedPosition.y + fingerB.normalizedPosition.y) / 2.0 * deviceSize.height); return [NSValue valueWithPoint:NSMakePoint(x, y)]; } - (void)endGestureWithEvent:(NSEvent *)event { CGFloat swipeMinimumLength = [TPCPreferences swipeMinimumLength]; if (swipeMinimumLength < 1.0) { return; } NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseAny inView:nil]; if (self.cachedSwipeOriginPoint == nil || touches.count != 2) { self.cachedSwipeOriginPoint = nil; return; } NSArray *touchArray = touches.allObjects; NSPoint origin = self.cachedSwipeOriginPoint.pointValue; NSPoint destination = [self touchesToPoint:touchArray[0] fingerB:touchArray[1]].pointValue; self.cachedSwipeOriginPoint = nil; NSPoint delta = NSMakePoint((origin.x - destination.x), (origin.y - destination.y)); if (fabs(delta.y) > fabs(delta.x)) { return; } if (fabs(delta.x) < swipeMinimumLength) { return; } CGFloat x = delta.x; BOOL invertedScrollingDirection = [RZUserDefaults() boolForKey:@"com.apple.swipescrolldirection"]; if (invertedScrollingDirection) { x = (x * (-1)); } if (x > 0) { [self selectPreviousWindow:nil]; } else { [self selectNextWindow:nil]; } } #pragma mark - #pragma mark Misc - (TVCMainWindowMouseLocation)locationOfMouseInWindow { NSPoint mouseLocation = [NSEvent mouseLocation]; return [self locationOfMouse:mouseLocation]; } - (TVCMainWindowMouseLocation)locationOfMouse:(NSPoint)mouseLocation { TVCMainWindowMouseLocation mouseLocationEnum = 0; NSRect windowFrame = self.frame; if (NSPointInRect(mouseLocation, windowFrame) == NO) { return mouseLocationEnum; } mouseLocationEnum |= TVCMainWindowMouseLocationInsideWindow; NSRect titlebarFrame = self.titlebarFrame; if (NSPointInRect(mouseLocation, titlebarFrame) == NO) { return mouseLocationEnum; } mouseLocationEnum |= TVCMainWindowMouseLocationInsideWindowTitle; #define ConvertRectToScreen(rect) \ NSMakeRect( (titlebarFrame.origin.x + rect.origin.x), \ (titlebarFrame.origin.y + rect.origin.y), \ rect.size.width, \ rect.size.height) \ #define PointInRect(view) \ NSPointInRect(mouseLocation, ConvertRectToScreen(view.frame)) if (PointInRect([self standardWindowButton:NSWindowCloseButton]) || PointInRect([self standardWindowButton:NSWindowMiniaturizeButton]) || PointInRect([self standardWindowButton:NSWindowZoomButton])) { mouseLocationEnum |= TVCMainWindowMouseLocationOnTopOfWindowTitleControl; return mouseLocationEnum; } for (NSTitlebarAccessoryViewController *viewController in self.titlebarAccessoryViewControllers) { /* NSTitlebarAccessoryViewController will have an origin of 0,0 which means we have to check the frame of it's superview, NSTitlebarAccessoryViewClipView */ if (PointInRect(viewController.view.superview) == NO) { continue; } mouseLocationEnum |= TVCMainWindowMouseLocationOnTopOfWindowTitleControl; return mouseLocationEnum; } return mouseLocationEnum; #undef ConvertRectToScreen #undef PointInRect } - (void)preferencesChanged { if ([TPCPreferences displayDockBadge] == NO) { [TVCDockIcon drawWithoutCount]; } else { [TVCDockIcon resetCachedCount]; [TVCDockIcon updateDockIcon]; } } - (void)endEditingFor:(nullable id)object { /* WebHTMLView results in this method being called. * * The documentation states "The endEditingFor: method should be used only as a * last resort if the field editor refuses to resign first responder status." * * The documentation then goes to say how you should try setting makeFirstResponder first. */ if ([self makeFirstResponder:self] == NO) { [super endEditingFor:object]; } } - (BOOL)canBecomeKeyWindow { return YES; } - (BOOL)canBecomeMainWindow { return YES; } - (BOOL)isDisabled { return NO; } - (void)makeKeyAndOrderFront:(nullable id)sender { if (self.disabled) { return; } [super makeKeyAndOrderFront:nil]; } - (void)orderFront:(nullable id)sender { if (self.disabled) { return; } [super orderFront:nil]; } - (NSRect)defaultWindowFrame { NSRect windowFrame = self.frame; windowFrame.size = self.userInterfaceObjects.defaultWindowSize; return windowFrame; } #pragma mark - #pragma mark Channel View Box - (BOOL)multipleItemsSelected { return (self.selectedItems.count > 1); } - (void)channelViewSelectionChangeTo:(IRCTreeItem *)selectedItem { [self selectItemInSelectedItems:selectedItem refreshChannelView:NO]; } - (void)updateChannelViewArrangement { [self.channelView updateArrangement]; } - (void)updateChannelViewBoxContentViewSelection { [self.channelView populateSubviews]; } - (BOOL)isItemVisible:(IRCTreeItem *)item { if (item == nil) { return NO; } return ([self isItemSelected:item] || [self isItemInSelectedGroup:item]); } - (BOOL)isItemSelected:(IRCTreeItem *)item { if (item == nil) { return NO; } return (self.selectedItem == item); } - (BOOL)isItemInSelectedGroup:(IRCTreeItem *)item { if (item == nil) { return NO; } return ([self.selectedItems containsObject:item]); } - (void)selectionDidChangeToRows:(NSIndexSet *)selectedRows { [self selectionDidChangeToRows:selectedRows selectedItem:nil]; } - (void)selectionDidChangeToRows:(NSIndexSet *)selectedRows selectedItem:(nullable IRCTreeItem *)selectedItem { NSParameterAssert(selectedRows != nil); /* Create list of selected items and notify those newly selected items that they are now visible + part of a stacked view */ NSArray *selectedItems = self.serverList.selectedObjects; /* Update selected item even if group hasn't changed */ if ([selectedItems isEqualToArray:self.selectedItems]) { /* Update selected item even if group hasn't changed */ if (selectedItem) { [self selectItemInSelectedItems:selectedItem]; } return; } NSUInteger selectedItemsCount = selectedItems.count; /* Store previous selection */ [self storePreviousSelection]; /* Update properties */ NSArray *selectedItemsPrevious = nil; if (self.selectedItems) { selectedItemsPrevious = [self.selectedItems copy]; } if (selectedItemsCount > 0) { self.selectedItems = selectedItems; if (selectedItem == nil) { selectedItem = self.selectedItem; } if (selectedItem && [self isItemInSelectedGroup:selectedItem]) { self.selectedItem = selectedItem; } else { self.selectedItem = selectedItems[(selectedItemsCount - 1)]; } } else { self.selectedItem = nil; self.selectedItems = @[]; } /* Update split view */ [self updateChannelViewBoxContentViewSelection]; /* Inform views that are currently selected that no longer will be that they are now hidden. We wait until after -updateChannelViewBoxContentViewSelection is called to do this so that the views that are hidden are actually hidden before informing the views of this fact. */ for (IRCTreeItem *item in selectedItemsPrevious) { if (selectedItems == nil || [selectedItems containsObject:item] == NO) { [item.viewController notifyDidBecomeHidden]; } } /* Inform new views that they are visible now that they are visible. */ for (IRCTreeItem *item in selectedItems) { if (selectedItemsPrevious == nil || [selectedItemsPrevious containsObject:item] == NO) { [item.viewController notifyDidBecomeVisible]; if (item != self.selectedItem) { [item.viewController notifySelectionChanged]; } } } selectedItems = nil; selectedItemsPrevious = nil; /* Perform postflight routines */ [self selectionDidChangePostflight]; } - (void)selectionDidChangePostflight { /* If the selection hasn't changed, then do nothing. */ IRCTreeItem *itemChangedTo = self.selectedItem; IRCTreeItem *itemChangedFrom = self.previouslySelectedItem; if (itemChangedTo == itemChangedFrom) { return; } /* Reset state of selections */ if (itemChangedFrom) { [itemChangedFrom resetState]; } if (itemChangedTo) { if (self.multipleItemsSelected) { [self.serverList refreshMessageCountForItem:itemChangedTo]; } [itemChangedTo resetState]; } /* Notify WebKit its selection status has changed */ if (itemChangedFrom) { [itemChangedFrom.viewController notifySelectionChanged]; } /* Destroy member list if we have no selection */ if (itemChangedTo == nil) { [self.memberList assignToChannel:nil]; self.serverList.menu = nil; [self updateTitle]; return; // Nothing more to do for empty selections } /* Prepare the member list for the selection */ BOOL isClient = itemChangedTo.isClient; BOOL isChannel = itemChangedTo.isChannel; /* The right click menu follows selection so let's update the menu we will show depending on the selection. */ if (isClient) { self.serverList.menu = menuController().mainMenuServerMenuItem.submenu; } else if (isChannel) { self.serverList.menu = menuController().mainMenuChannelMenu; } else { self.serverList.menu = menuController().mainMenuQueryMenu; } /* Update table view data sources */ if (isChannel) { [self.memberList assignToChannel:(id)itemChangedTo]; } else { [self.memberList assignToChannel:nil]; } /* Begin work on text field */ BOOL autoFocusInputTextField = [TPCPreferences focusMainTextViewOnSelectionChange]; if (autoFocusInputTextField && [XRAccessibility isVoiceOverEnabled] == NO) { [self.inputTextField focus]; } [self.inputTextField updateSegmentedController]; /* Setup text field value with history item when we have history setup to be channel specific. */ [self.inputHistoryManager moveFocusTo:itemChangedTo]; /* Reset spelling for text field */ [self.inputTextField resetSpellingIgnores]; /* Update splitter view depending on selection */ if (isChannel) { if (self.memberList.isHiddenByUser == NO) { [self.contentSplitView expandMemberList]; } } else { [self.contentSplitView collapseMemberList]; } /* Notify WebKit its selection status has changed */ [itemChangedTo.viewController notifySelectionChanged]; /* Finish up */ [self storeLastSelectedChannel]; [RZNotificationCenter() postNotificationName:TVCMainWindowSelectionChangedNotification object:self]; [TVCDockIcon updateDockIcon]; [self updateTitle]; } #pragma mark - #pragma mark Split View - (void)saveContentSplitViewState { [RZUserDefaults() setBool:self.serverListVisible forKey:@"Window -> Main Window -> Server List is Visible"]; [RZUserDefaults() setBool:(self.memberList.isHiddenByUser == NO) forKey:@"Window -> Main Window -> Member List is Visible"]; } - (void)restoreSavedContentSplitViewState { /* Make server list and member list visible + restore saved position. */ [self.contentSplitView restorePositions]; /* Collapse one or more items if they were collapsed when closing Textual. */ id makeMemberListVisible = [RZUserDefaults() objectForKey:@"Window -> Main Window -> Member List is Visible"]; if (makeMemberListVisible && [makeMemberListVisible boolValue] == NO) { self.memberList.isHiddenByUser = YES; [self.contentSplitView collapseMemberList]; } id makeServerListVisible = [RZUserDefaults() objectForKey:@"Window -> Main Window -> Server List is Visible"]; if (makeServerListVisible && [makeServerListVisible boolValue] == NO) { [self.contentSplitView collapseServerList]; } } - (BOOL)isMemberListVisible { return (self.contentSplitView.memberListCollapsed == NO); } - (BOOL)isServerListVisible { return (self.contentSplitView.serverListCollapsed == NO); } #pragma mark - #pragma mark License Manager #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 - (void)licenseManagerActivatedLicense:(NSNotification *)notification { [self reloadLoadingScreen]; } - (void)licenseManagerDeactivatedLicense:(NSNotification *)notification { [self reloadLoadingScreen]; } - (void)licenseManagerTrialExpired:(NSNotification *)notification { [self reloadLoadingScreen]; } #endif #pragma mark - #pragma mark Loading Screen - (void)setLoadingScreenProgressViewReason:(NSString *)progressReason { NSParameterAssert(progressReason != nil); [self.loadingScreen setProgressViewReason:progressReason]; } - (BOOL)reloadLoadingScreen { /* This method returns YES (success) if the loading screen is dismissed when called. NO indicates an error that resulted in it staying on screen. */ if (worldController().isImportingConfiguration) { return NO; } if (masterController().applicationIsLaunched == NO) { [self.loadingScreen showProgressViewWithReason:TXTLS(@"TVCMainWindow[iph-a9]")]; return NO; } #if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1 if (TLOLicenseManagerTextualIsRegistered() == NO && TLOLicenseManagerIsTrialExpired()) { [self.loadingScreen showTrialExpiredView]; return NO; } #endif if (worldController().clientCount <= 0) { [self.loadingScreen showWelcomeAddServerView]; return NO; } [self.loadingScreen hideAnimated]; return YES; } #pragma mark - #pragma mark Window Extras - (void)presentCertificateTrustInformation:(id)sender { IRCClient *u = self.selectedClient; if (u) { [u presentCertificateTrustInformation]; } } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 - (void)titlebarAccessoryViewLockButtonClicked:(id)sender { NSMenu *statusMenu = menuController().encryptionManagerStatusMenu; [statusMenu popUpMenuPositioningItem:nil atLocation:self.titlebarAccessoryViewLockButton.frame.origin inView:self.titlebarAccessoryViewLockButton]; } #endif - (void)updateAccessoryViewLockButton { IRCClient *u = self.selectedClient; #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 IRCChannel *c = self.selectedChannel; BOOL updateEncryption = (c.isPrivateMessage && [u encryptionAllowedForTarget:c.name]); if (updateEncryption) { self.titlebarAccessoryViewLockButton.action = @selector(titlebarAccessoryViewLockButtonClicked:); [self.titlebarAccessoryViewLockButton enableDrawingCustomBackgroundColor]; [self.titlebarAccessoryViewLockButton positionImageOnLeftSide]; [sharedEncryptionManager() updateLockIconButton:self.titlebarAccessoryViewLockButton withStateOf:[u encryptionAccountNameForUser:c.name] from:[u encryptionAccountNameForLocalUser]]; self.titlebarAccessoryView.hidden = NO; } else { #endif self.titlebarAccessoryViewLockButton.action = @selector(presentCertificateTrustInformation:); [self.titlebarAccessoryViewLockButton disableDrawingCustomBackgroundColor]; [self.titlebarAccessoryViewLockButton positionImageOverContent]; self.titlebarAccessoryViewLockButton.title = @""; if (u.isSecured) { [self.titlebarAccessoryViewLockButton setIconAsLocked]; self.titlebarAccessoryView.hidden = NO; } else { self.titlebarAccessoryView.hidden = YES; } #if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1 } #endif if (self.titlebarAccessoryView.hidden == NO) { [self.titlebarAccessoryViewLockButton sizeToFit]; } } - (void)addAccessoryViewsToTitlebar { NSThemeFrame *themeFrame = (NSThemeFrame *)self.contentView.superview; themeFrame.usesCustomTitlebarTitlePositioning = YES; NSTitlebarAccessoryViewController *accessoryView = self.titlebarAccessoryViewController; accessoryView.layoutAttribute = NSLayoutAttributeRight; [self addTitlebarAccessoryViewController:accessoryView]; } - (void)updateTitleFor:(IRCTreeItem *)item { NSParameterAssert(item != nil); if ([self isItemSelected:item] == NO) { return; } [self updateTitle]; } - (void)updateTitle { [self updateAccessoryViewLockButton]; IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil && c == nil) { self.title = [TPCApplicationInfo applicationName]; return; } NSMutableString *title = [NSMutableString string]; if (u.isConnected == NO && u.isConnecting == NO) { if (u.isReconnecting) { [title appendString:TXTLS(@"TVCMainWindow[3yn-wd]")]; } else { [title appendString:TXTLS(@"TVCMainWindow[q42-an]")]; } } else if (u.isConnecting && u.isLoggedIn == NO) { if (u.connectType == IRCClientConnectModeRetry || u.connectType == IRCClientConnectModeReconnect) { [title appendString:TXTLS(@"TVCMainWindow[s23-zd]")]; } else { [title appendString:TXTLS(@"TVCMainWindow[8eu-c7]")]; } } else if (u.isConnected && u.isLoggedIn == NO) { [title appendString:TXTLS(@"TVCMainWindow[wcb-y8]")]; } else if (u.isQuitting) { [title appendString:TXTLS(@"TVCMainWindow[xqd-h9]")]; } NSString *awayStatus = ((u.userIsAway) ? TXTLS(@"TVCMainWindow[nxz-l9]") : @""); [title appendString:TXTLS(@"TVCMainWindow[19v-bc]", u.userNickname, awayStatus, u.networkNameAlt)]; if (c == nil) // = Client { /* If we have the actual server that the client is connected to, then we we append that. Otherwise, we just leave it blank. */ NSString *serverAddress = u.serverAddress; if (serverAddress) { [title appendString:TXTLS(@"TVCMainWindow[jqk-ha]")]; // divider [title appendString:serverAddress]; } } else { [title appendString:TXTLS(@"TVCMainWindow[jqk-ha]")]; // divider NSString *channelName = c.name; switch (c.type) { case IRCChannelTypeChannel: { [title appendString:channelName]; NSString *userCount = TXFormattedNumber(c.numberOfMembers); [title appendString:TXTLS(@"TVCMainWindow[v6i-zb]", userCount)]; NSString *modeSymbols = c.modeInfo.stringWithMaskedPassword; if (modeSymbols.length > 1) { [title appendString:TXTLS(@"TVCMainWindow[cyg-g9]", modeSymbols)]; } break; } case IRCChannelTypePrivateMessage: { /* Textual defines the topic of a private message as the user host. */ /* If it is not defined yet, then we just use the channel name which is equal to the nickname of the private message owner. */ IRCUser *user = [u findUser:channelName]; NSString *hostmask = user.hostmaskFragment; if (hostmask) { [title appendString:TXTLS(@"TVCMainWindow[6wz-pd]", channelName, hostmask)]; } else { [title appendString:channelName]; } break; } case IRCChannelTypeUtility: { [title appendString:channelName]; break; } } } self.title = title; [self setAccessibilityTitle:TXTLS(@"Accessibility[k79-1a]")]; } #pragma mark - #pragma mark User List - (void)updateDrawingForUserInUserList:(IRCUser *)user { IRCChannel *selectedChannel = self.selectedChannel; if (selectedChannel == nil) { return; } IRCChannelUser *channelUser = [user userAssociatedWithChannel:selectedChannel]; if (channelUser == nil) { return; } [self.memberList refreshDrawingForMember:channelUser]; } #pragma mark - #pragma mark Server List - (void)saveSelection { NSMutableArray *selectedIdentifiers = [NSMutableArray array]; for (IRCTreeItem *item in self.selectedItems) { [selectedIdentifiers addObject:item.uniqueIdentifier]; } [RZUserDefaults() setObject:[selectedIdentifiers copy] forKey:@"Window -> Main Window -> Server List Selection"]; } - (void)restoreExpandedClients { for (IRCClient *e in worldController().clientList) { if (e.config.sidebarItemExpanded) { [self expandClient:e]; } } } - (void)restoreSelectionDuringSetup { NSArray *selectedIdentifiers = [RZUserDefaults() objectForKey:@"Window -> Main Window -> Server List Selection"]; if (selectedIdentifiers == nil || selectedIdentifiers.count == 0) { [self selectBestChoiceDuringSetup]; return; } NSArray *selection = [worldController() findItemsWithIds:selectedIdentifiers]; if (selection.count == 0) { [self selectBestChoiceDuringSetup]; return; } [self adjustSelectionWithItems:selection selectedItem:nil]; } - (void)selectBestChoiceDuringSetup { IRCClient *firstSelection = nil; for (IRCClient *e in worldController().clientList) { if (e.config.autoConnect && e.config.sidebarItemExpanded) { if (firstSelection == nil) { firstSelection = e; } } } if (firstSelection) { NSInteger n = [self.serverList rowForItem:firstSelection]; if (firstSelection.channelCount > 0) { n++; } [self.serverList selectItemAtIndex:n]; } else { [self.serverList selectItemAtIndex:0]; } } - (void)setupTrees { self.memberList.keyDelegate = self; self.memberList.target = menuController(); self.memberList.doubleAction = @selector(memberInMemberListDoubleClicked:); self.serverList.keyDelegate = self; self.serverList.delegate = (id)self; self.serverList.dataSource = (id)self; self.serverList.target = self; self.serverList.doubleAction = @selector(outlineViewDoubleClicked:); /* Inform the table we want drag events */ [self.serverList registerForDraggedTypes:_treeDragItemTypes]; /* Prepare our first selection */ [self restoreExpandedClients]; [self restoreSelectionDuringSetup]; /* Fake the delegate call */ [self outlineViewSelectionDidChange:nil]; /* Populate navigation list */ [menuController() populateNavigationChannelList]; } - (nullable IRCClient *)selectedClient { if ( self.selectedItem) { return self.selectedItem.associatedClient; } else { return nil; } } - (nullable IRCChannel *)selectedChannel { if ( self.selectedItem) { if (self.selectedItem.isClient) { return nil; } else { return (id)self.selectedItem; } } else { return nil; } } - (nullable IRCChannel *)selectedChannelOn:(IRCClient *)c { if (self.selectedClient == c) { return self.selectedChannel; } else { return nil; } } - (nullable TVCLogController *)selectedViewController { if ( self.selectedChannel) { return self.selectedChannel.viewController; } else if (self.selectedClient) { return self.selectedClient.viewController; } else { return nil; } } - (void)reloadTreeItem:(IRCTreeItem *)item { NSParameterAssert(item != nil); [self.serverList refreshDrawingForItem:item]; } - (void)reloadTreeGroup:(IRCTreeItem *)item { NSParameterAssert(item != nil); if (item.isClient == NO) { return; } [self reloadTreeItem:item]; for (IRCChannel *channel in ((IRCClient *)item).channelList) { [self reloadTreeItem:channel]; } } - (void)reloadTree { [self.serverList refreshAllDrawings]; } - (void)expandClient:(IRCClient *)client { [[self.serverList animator] expandItem:client]; } - (void)adjustSelection { [self adjustSelectionWithItems:self.selectedItems selectedItem:self.selectedItem]; } - (void)adjustSelectionWithItems:(NSArray *)selectedItems selectedItem:(nullable IRCTreeItem *)selectedItem { NSParameterAssert(selectedItems != nil); NSMutableIndexSet *itemRows = [NSMutableIndexSet indexSet]; for (IRCTreeItem *item in selectedItems) { /* Expand the parent of the item if its not already expanded. */ if (item.isClient == NO) { IRCClient *itemClient = item.associatedClient; [self.serverList expandItem:itemClient]; } /* Find the row of the item */ NSInteger itemRow = [self.serverList rowForItem:item]; if ( itemRow >= 0) { [itemRows addIndex:itemRow]; } } /* If the selected rows have not changed, then only select the one item */ NSIndexSet *selectedRows = self.serverList.selectedRowIndexes; if ([selectedRows isEqualToIndexSet:itemRows] == NO) { /* Selection updates are disabled and selection changes are faked so that the correct next item is selected when moving to previous group. */ self.ignoreNextOutlineViewSelectionChange = YES; [self.serverList selectRowIndexes:itemRows byExtendingSelection:NO scrollToSelection:YES]; } /* Perform selection logic */ [self selectionDidChangeToRows:itemRows selectedItem:selectedItem]; } - (void)storePreviousSelection { self.previousSelectedItemId = self.selectedItem.uniqueIdentifier; [self storePreviousSelections]; } - (void)storePreviousSelections { NSMutableArray *previousSelectedItems = [NSMutableArray array]; for (IRCTreeItem *item in self.selectedItems) { [previousSelectedItems addObject:item.uniqueIdentifier]; } self.previousSelectedItemsId = previousSelectedItems; } - (void)storeLastSelectedChannel { if (self.selectedClient) { self.selectedClient.lastSelectedChannel = self.selectedChannel; } } - (nullable IRCTreeItem *)previouslySelectedItem { NSString *itemIdentifier = self.previousSelectedItemId; if (itemIdentifier) { return [worldController() findItemWithId:itemIdentifier]; } return nil; } - (void)selectPreviousItem { /* Do not try to browse backwards without these items */ if (self.previousSelectedItemId == nil || self.previousSelectedItemsId == nil) { return; } /* Get previously selected item and cancel if its missing */ IRCTreeItem *itemPrevious = self.previouslySelectedItem; if (itemPrevious == nil) { return; } /* Build list of rows in the table view that contain previous group */ NSMutableArray *itemsPrevious = [NSMutableArray array]; for (NSString *itemIdentifier in self.previousSelectedItemsId) { IRCTreeItem *item = [worldController() findItemWithId:itemIdentifier]; if ( item) { [itemsPrevious addObject:item]; } } [self adjustSelectionWithItems:itemsPrevious selectedItem:itemPrevious]; } - (void)selectItemInSelectedItems:(IRCTreeItem *)selectedItem { [self selectItemInSelectedItems:selectedItem refreshChannelView:YES]; } - (void)selectItemInSelectedItems:(IRCTreeItem *)selectedItem refreshChannelView:(BOOL)refreshChannelView { NSParameterAssert(selectedItem != nil); /* Do nothing if items are the same */ if ([self isItemSelected:selectedItem]) { return; } /* Select item if its in the current group */ if ([self isItemInSelectedGroup:selectedItem] == NO) { return; } [self storePreviousSelection]; self.selectedItem = selectedItem; if (refreshChannelView) { [self updateChannelViewBoxContentViewSelection]; } [self selectionDidChangePostflight]; } - (void)select:(nullable IRCTreeItem *)item { [self shiftSelection:self.selectedItem toItem:item options:(TVCMainWindowShiftSelectionFlagMaintainGrouping | TVCMainWindowShiftSelectionFlagPerformDeselect)]; } - (void)deselect:(IRCTreeItem *)item { NSParameterAssert(item != nil); [self shiftSelection:item toItem:nil options:TVCMainWindowShiftSelectionFlagPerformDeselect]; } - (void)deselectGroup:(IRCTreeItem *)item { NSParameterAssert(item != nil); if (item.isClient == NO) { return; } [self shiftSelection:item toItem:nil options:(TVCMainWindowShiftSelectionFlagPerformDeselect | TVCMainWindowShiftSelectionFlagPerformDeselectChildren)]; } - (void)shiftSelection:(nullable IRCTreeItem *)oldItem toItem:(nullable IRCTreeItem *)newItem options:(TVCMainWindowShiftSelectionFlags)selectionOptions { if (oldItem == newItem) { return; } /* If the next item is a channel, then make sure the client it is associated with is expanded, or we can't switch to it. */ if (newItem && newItem.isClient == NO) { IRCClient *itemClient = newItem.associatedClient; [self expandClient:itemClient]; } /* Context */ BOOL optionMaintainGrouping = ((selectionOptions & TVCMainWindowShiftSelectionFlagMaintainGrouping) == TVCMainWindowShiftSelectionFlagMaintainGrouping); BOOL optionPerformDeselectAll = NO; BOOL optionPerformDeselectOld = ((selectionOptions & TVCMainWindowShiftSelectionFlagPerformDeselect) == TVCMainWindowShiftSelectionFlagPerformDeselect); BOOL optionPerformDeselectChildren = ((selectionOptions & TVCMainWindowShiftSelectionFlagPerformDeselectChildren) == TVCMainWindowShiftSelectionFlagPerformDeselectChildren); BOOL optionPerformDeselect = (optionPerformDeselectChildren || optionPerformDeselectOld); /* Do nothing if item is not group */ NSInteger itemIndexOld = [self.serverList rowForItem:oldItem]; NSInteger itemIndexNew = [self.serverList rowForItem:newItem]; NSIndexSet *selectedRows = self.serverList.selectedRowIndexes; NSIndexSet *selectedRowsForbidden = nil; /* Maybe do nothing at all */ if (optionPerformDeselect && itemIndexOld >= 0 && [selectedRows containsIndex:itemIndexOld] == NO) { return; } /* If we are not performing a deselect for the old item and both items are selected, then simply update selection inside grouping. */ if (optionMaintainGrouping && (itemIndexOld >= 0 && [selectedRows containsIndex:itemIndexOld]) && (itemIndexNew >= 0 && [selectedRows containsIndex:itemIndexNew]) && newItem != nil) // This condition is impossible but static analyzer doesn't know that. // Condition is impossible because itemIndexNew will never return // greater to or equal zero unless item is non-nil. { [self selectItemInSelectedItems:newItem]; return; } else { if (optionPerformDeselectOld) { optionPerformDeselectAll = YES; } } /* Create a mutable copy of the current selection */ NSMutableIndexSet *selectedRowsNew = [selectedRows mutableCopy]; if (optionPerformDeselectAll) { [selectedRowsNew removeAllIndexes]; } else if (optionPerformDeselectOld) { [selectedRowsNew removeIndex:itemIndexOld]; } /* optionPerformDeselectChildren is still performed even if optionPerformDeselectAll is set so that the list of forbidden rows can be defined by it. */ if (optionPerformDeselectChildren) { NSIndexSet *childrenRowRange = [self.serverList indexesOfItemsInGroup:oldItem]; if (childrenRowRange) { [selectedRowsNew removeIndexes:childrenRowRange]; selectedRowsForbidden = childrenRowRange; } } /* If the next item is not nil and is a row, then select that */ if (newItem) { if (itemIndexNew >= 0) { [selectedRowsNew addIndex:itemIndexNew]; } else { LogToConsoleDebug("Tried to shift selection to an item not in the server list"); return; } } /* If no item to switch to is specified, then the current action is treated as a deselect for the old item. In that case, we pick the next best item to remain selected. */ if (newItem == nil) { /* If there is an item in the current selection that is before or after the row removed, then we can use that. */ BOOL selectedRowsComplete = ([selectedRowsNew indexLessThanIndex:itemIndexOld] != NSNotFound || [selectedRowsNew indexGreaterThanIndex:itemIndexOld] != NSNotFound); /* If there is not an item in the current selection that can take over, then the first step is to try to find an item newer than the current. */ if (selectedRowsComplete == NO) { NSInteger numberOfRows = self.serverList.numberOfRows; NSInteger nextSelectionRow = (itemIndexOld + 1); /* Next row is in forbidden range */ if (selectedRowsForbidden && [selectedRowsForbidden containsIndex:nextSelectionRow]) { nextSelectionRow = (selectedRowsForbidden.lastIndex + 1); } /* Next row is above number of rows. Try to go one below instead. */ if (nextSelectionRow >= numberOfRows) { nextSelectionRow = (itemIndexOld - 1); } /* Previous row is in forbidden range */ if (selectedRowsForbidden && [selectedRowsForbidden containsIndex:nextSelectionRow]) { nextSelectionRow = (selectedRowsForbidden.firstIndex - 1); } /* Previous row is less than zero. There is no where else to go. */ if (nextSelectionRow < 0) { nextSelectionRow = (-1); } /* Add new selection index if there is one. */ if (nextSelectionRow >= 0) { [selectedRowsNew addIndex:nextSelectionRow]; } } } /* Save selection */ if (selectedRowsNew.count == 0) { [self storePreviousSelection]; self.selectedItem = nil; self.selectedItems = @[]; [self selectionDidChangePostflight]; return; } [self.serverList selectRowIndexes:selectedRowsNew byExtendingSelection:NO scrollToSelection:YES]; } #pragma mark - #pragma mark Server List Delegate - (void)outlineViewDoubleClicked:(id)sender { IRCClient *u = self.selectedClient; IRCChannel *c = self.selectedChannel; if (u == nil && c == nil) { return; } if (u && c == nil) { if (u.isConnecting || u.isConnected) { if ([TPCPreferences disconnectOnDoubleclick]) { [u quit]; } } else if (u.isQuitting) { LogToConsole("Double click event ignored because client is quitting"); } else { if ([TPCPreferences connectOnDoubleclick]) { [u connect]; } } [self expandClient:u]; } else { if (u.isLoggedIn == NO) { return; } if (c.isActive) { if ([TPCPreferences leaveOnDoubleclick]) { [u partChannel:c]; } } else { if ([TPCPreferences joinOnDoubleclick]) { [u joinChannel:c]; } } } } - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(nullable id)item { if (item) { return [item numberOfChildren]; } return worldController().clientCount; } - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { return ([item numberOfChildren] > 0); } - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(nullable id)item { if (item) { return [item childAtIndex:index]; } return worldController().clientList[index]; } - (nullable id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn byItem:(nullable id)item { return item; } - (nullable NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item { if (item == nil || [item isClient]) { return [[TVCServerListGroupRowCell alloc] initWithServerList:(id)outlineView]; } else { return [[TVCServerListChildRowCell alloc] initWithServerList:(id)outlineView]; } } - (nullable NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(nullable NSTableColumn *)tableColumn item:(id)item { NSString *viewIdentifier = nil; if (item == nil || [item isClient]) { viewIdentifier = @"GroupView"; } else { viewIdentifier = @"ChildView"; } NSView *newView = [outlineView makeViewWithIdentifier:viewIdentifier owner:self]; return newView; } - (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row { [self.serverList refreshDrawingForRow:row]; } - (void)outlineViewItemDidCollapse:(NSNotification *)notification { id itemBeingCollapsed = notification.userInfo[@"NSObject"]; IRCClient *u = [itemBeingCollapsed associatedClient]; u.sidebarItemIsExpanded = NO; } - (void)outlineViewItemDidExpand:(NSNotification *)notification { id itemBeingCollapsed = notification.userInfo[@"NSObject"]; IRCClient *u = [itemBeingCollapsed associatedClient]; u.sidebarItemIsExpanded = YES; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldExpandItem:(id)item { return YES; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldCollapseItem:(id)item { return YES; } - (void)outlineViewItemWillCollapse:(NSNotification *)notification { } - (BOOL)selectionShouldChangeInOutlineView:(NSOutlineView *)outlineView { TVCServerList *serverList = (id)outlineView; /* Allow rows to be deselected during redrawing */ /* See logic in -updateAppearance in TVCServerList */ if (serverList.invalidatingBackgroundForSelection) { return YES; } /* If the window is not focused, don't allow change. */ if (self.keyWindow == NO) { return NO; } /* If the server list does not have a mouse down event, allow change. */ if (serverList.leftMouseIsDownInView == NO) { return YES; } /* If command or shift are held down, allow change. */ NSUInteger keyboardKeys = ([NSEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask); if ((keyboardKeys & NSEventModifierFlagCommand) == NSEventModifierFlagCommand || (keyboardKeys & NSEventModifierFlagShift) == NSEventModifierFlagShift) { return YES; } /* Find which row is beneath the mouse */ NSInteger rowBeneathMouse = outlineView.rowBeneathMouse; /* If a row is not beneath the mouse or the row that is, is not selected, then the selection is allowed to be changed. */ if (rowBeneathMouse < 0) { return YES; } if ([outlineView isRowSelected:rowBeneathMouse] == NO) { return YES; } /* If the item beneath the mouse is already selected and we did not try to unselect it by holding command or shift, then tell the table view not to change the selection. That will be handled by us. */ IRCTreeItem *itemUnderMouse = [outlineView itemAtRow:rowBeneathMouse]; [self selectItemInSelectedItems:itemUnderMouse]; return NO; } - (NSIndexSet *)outlineView:(NSOutlineView *)outlineView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes { #define _maximumSelectedRows 6 return [outlineView selectionIndexesForProposedSelection:proposedSelectionIndexes maximumNumberOfSelections:_maximumSelectedRows]; #undef _maximumSelectedRows } - (void)outlineViewSelectionDidChange:(NSNotification *)notification { TVCServerList *serverList = (id)((notification.object) ?: self.serverList); if (serverList.invalidatingBackgroundForSelection) { return; } if (self.ignoreNextOutlineViewSelectionChange) { self.ignoreNextOutlineViewSelectionChange = NO; return; } if (self.ignoreOutlineViewSelectionChanges) { return; } NSIndexSet *selectedRows = serverList.selectedRowIndexes; IRCTreeItem *selectedItem = nil; NSUInteger keyboardKeys = ([NSEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask); if (keyboardKeys == NSEventModifierFlagCommand) { NSInteger rowBeneathMouse = serverList.rowBeneathMouse; if (rowBeneathMouse >= 0 && [selectedRows containsIndex:rowBeneathMouse]) { selectedItem = [serverList itemAtRow:rowBeneathMouse]; } } if (selectedItem) { [self selectionDidChangeToRows:selectedRows selectedItem:selectedItem]; } else { [self selectionDidChangeToRows:selectedRows]; } } - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pasteboard { /* TODO (March 27, 2016): Support dragging multiple items */ if (items.count == 1) { NSString *itemToken = [worldController() pasteboardStringForItem:items[0]]; [pasteboard declareTypes:_treeDragItemTypes owner:self]; [pasteboard setString:itemToken forType:_treeDragItemType]; } return YES; } - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id )info proposedItem:(nullable id)item proposedChildIndex:(NSInteger)index { if (index < 0) { return NSDragOperationNone; } NSPasteboard *pasteboard = [info draggingPasteboard]; if ([pasteboard availableTypeFromArray:_treeDragItemTypes] == nil) { return NSDragOperationNone; } NSString *draggedItemToken = [pasteboard stringForType:_treeDragItemType]; if (draggedItemToken == nil) { return NSDragOperationNone; } IRCTreeItem *draggedItem = [worldController() findItemWithPasteboardString:draggedItemToken]; if (draggedItem == nil) { return NSDragOperationNone; } if (draggedItem.isClient) { if (item) { return NSDragOperationNone; } } else { IRCChannel *channel = (IRCChannel *)draggedItem; if (channel.associatedClient != item) { return NSDragOperationNone; } IRCClient *client = (IRCClient *)item; NSArray *channelList = client.channelList; IRCChannel *previousItem = nil; if ((index - 1) >= 0) { previousItem = channelList[(index - 1)]; } IRCChannel *nextItem = nil; if (index < channelList.count) { nextItem = channelList[index]; } if (channel.isChannel) { if (previousItem && previousItem.isChannel == NO) { return NSDragOperationNone; } } else { if (nextItem.isChannel) { return NSDragOperationNone; } } } return NSDragOperationGeneric; } - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id )info item:(nullable id)item childIndex:(NSInteger)index { if (index < 0) { return NSDragOperationNone; } NSPasteboard *pasteboard = [info draggingPasteboard]; if ([pasteboard availableTypeFromArray:_treeDragItemTypes] == nil) { return NSDragOperationNone; } NSString *draggedItemToken = [pasteboard stringForType:_treeDragItemType]; if (draggedItemToken == nil) { return NSDragOperationNone; } IRCTreeItem *draggedItem = [worldController() findItemWithPasteboardString:draggedItemToken]; if (draggedItem == nil) { return NSDragOperationNone; } TVCServerList *serverList = (id)outlineView; if (draggedItem.isClient) { NSArray *clientList = worldController().clientList; NSMutableArray *clientListMutable = [clientList mutableCopy]; NSUInteger originalIndex = [clientList indexOfObjectIdenticalTo:draggedItem]; [clientListMutable moveObjectAtIndex:originalIndex toIndex:index]; worldController().clientList = clientListMutable; [serverList moveItemAtIndex:originalIndex inParent:nil toIndex:index inParent:nil]; } else { if (item == nil || item != draggedItem.associatedClient) { return NO; } IRCClient *client = (IRCClient *)item; NSArray *channelList = client.channelList; NSMutableArray *channelListMutable = [channelList mutableCopy]; NSUInteger originalIndex = [channelList indexOfObjectIdenticalTo:draggedItem]; [channelListMutable moveObjectAtIndex:originalIndex toIndex:index]; client.channelList = channelListMutable; [serverList moveItemAtIndex:originalIndex inParent:client toIndex:index inParent:client]; } [menuController() populateNavigationChannelList]; return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindowAppearance.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCAppearancePrivate.h" #import "TVCServerListAppearancePrivate.h" #import "TVCMemberListAppearancePrivate.h" #import "TVCMainWindowTextViewAppearancePrivate.h" #import "TVCMainWindow.h" #import "TVCMainWindowAppearancePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowAppearance () @property (nonatomic, strong, readwrite) TVCServerListAppearance *serverList; @property (nonatomic, strong, readwrite) TVCMemberListAppearance *memberList; @property (nonatomic, strong, readwrite) TVCMainWindowTextViewAppearance *textView; @property (nonatomic, assign, readwrite) NSSize defaultWindowSize; @property (nonatomic, copy, nullable, readwrite) NSColor *channelViewOverlayDefaultBackgroundColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelViewOverlayDefaultBackgroundColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *loadingScreenBackgroundColor; @property (nonatomic, copy, nullable, readwrite) NSColor *splitViewDividerColor; @property (nonatomic, copy, nullable, readwrite) NSColor *titlebarAccessoryViewBackgroundColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *titlebarAccessoryViewBackgroundColorInactiveWindow; @property (nonatomic, assign, readwrite) CGFloat titlebarAccessoryViewLeftMargin; @property (nonatomic, assign, readwrite) CGFloat titlebarAccessoryViewRightMargin; @end @implementation TVCMainWindowAppearance #pragma mark - #pragma mark Initialization - (nullable instancetype)initWithWindow:(TVCMainWindow *)mainWindow { NSParameterAssert(mainWindow != nil); NSURL *appearanceLocation = [self.class appearanceLocation]; BOOL forRetinaDisplay = mainWindow.runningInHighResolutionMode; if ((self = [super initWithAppearanceAtURL:appearanceLocation forRetinaDisplay:forRetinaDisplay])) { [self prepareInitialStateWithWindow:mainWindow]; return self; } return nil; } + (NSURL *)appearanceLocation { return [RZMainBundle() URLForResource:@"TVCMainWindowAppearance" withExtension:@"plist"]; } - (void)prepareInitialStateWithWindow:(TVCMainWindow *)mainWindow { NSParameterAssert(mainWindow != nil); self.defaultWindowSize = [self sizeForKey:@"defaultWindowSize"]; self.channelViewOverlayDefaultBackgroundColorActiveWindow = [self colorForKey:@"channelViewOverlayDefaultBackgroundColor" forActiveWindow:YES]; self.channelViewOverlayDefaultBackgroundColorInactiveWindow = [self colorForKey:@"channelViewOverlayDefaultBackgroundColor" forActiveWindow:NO]; self.loadingScreenBackgroundColor = [self colorForKey:@"loadingScreenBackgroundColor"]; self.splitViewDividerColor = [self colorForKey:@"splitViewDividerColor"]; self.titlebarAccessoryViewBackgroundColorActiveWindow = [self colorForKey:@"titlebarAccessoryViewBackgroundColor" forActiveWindow:YES]; self.titlebarAccessoryViewBackgroundColorInactiveWindow = [self colorForKey:@"titlebarAccessoryViewBackgroundColor" forActiveWindow:NO]; self.titlebarAccessoryViewLeftMargin = [self measurementForKey:@"titlebarAccessoryViewLeftMargin"]; self.titlebarAccessoryViewRightMargin = [self measurementForKey:@"titlebarAccessoryViewRightMargin"]; self.serverList = [[TVCServerListAppearance alloc] initWithServerList:mainWindow.serverList inWindow:mainWindow]; self.memberList = [[TVCMemberListAppearance alloc] initWithMemberList:mainWindow.memberList inWindow:mainWindow]; self.textView = [[TVCMainWindowTextViewAppearance alloc] initWithWindow:mainWindow]; [self flushAppearanceProperties]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindowChannelView.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXAppearance.h" #import "NSViewHelperPrivate.h" #import "TPCPreferencesLocal.h" #import "TPCThemeController.h" #import "TPCTheme.h" #import "IRCTreeItem.h" #import "TVCLogController.h" #import "TVCLogView.h" #import "TVCMainWindowAppearance.h" #import "TVCMainWindowPrivate.h" #import "TVCMainWindowSplitViewPrivate.h" #import "TVCMainWindowChannelViewPrivate.h" NS_ASSUME_NONNULL_BEGIN @class TVCMainWindowChannelViewSubviewOverlayView; @interface TVCMainWindowChannelViewSubview : NSView @property (nonatomic, assign) NSUInteger itemIndex; @property (nonatomic, assign) BOOL isSelected; @property (nonatomic, assign) BOOL overlayVisible; @property (nonatomic, assign) BOOL isObservingBackingView; @property (readonly) BOOL backingViewIsLoading; @property (nonatomic, copy) NSString *uniqueIdentifier; @property (nonatomic, strong, nullable) TVCLogView *backingView; @property (nonatomic, weak) TVCMainWindowChannelView *parentView; @property (nonatomic, strong, nullable) TVCMainWindowChannelViewSubviewOverlayView *overlayView; - (void)toggleOverlayView; @end @interface TVCMainWindowChannelViewSubviewOverlayView : NSView @end @interface TVCMainWindowChannelView () @property (nonatomic, assign) BOOL isMovingDividers; @property (nonatomic, assign) NSUInteger itemIndexSelected; - (void)selectionChangeTo:(NSUInteger)itemIndex; @end @implementation TVCMainWindowChannelView NSComparisonResult sortSubviews(TVCMainWindowChannelViewSubview *firstView, TVCMainWindowChannelViewSubview *secondView, void *context) { NSUInteger itemIndex1 = firstView.itemIndex; NSUInteger itemIndex2 = secondView.itemIndex; if (itemIndex1 < itemIndex2) { return NSOrderedAscending; } else if (itemIndex1 > itemIndex2) { return NSOrderedDescending; } return NSOrderedSame; } - (void)awakeFromNib { [super awakeFromNib]; self.delegate = (id)self; } - (void)viewDidMoveToWindow { if (self.window == nil) { [RZNotificationCenter() removeObserver:self]; return; } [RZNotificationCenter() addObserver:self selector:@selector(themeAppearanceChanged:) name:TPCThemeAppearanceChangedNotification object:nil]; } - (void)resetSubviews { NSArray *subviews = [self.subviews copy]; for (NSView *subview in subviews) { [subview removeFromSuperview]; } } - (void)populateSubviews { /* Get list of views selected by the user */ TVCMainWindow *mainWindow = self.mainWindow; NSArray *selectedItems = mainWindow.selectedItems; NSUInteger selectedItemsCount = selectedItems.count; if (selectedItemsCount == 0) { [self resetSubviews]; self.itemIndexSelected = NSNotFound; return; } /* Make a list of subviews that already exist to compare when adding or removing views so that we do not have to destroy entire backing. */ NSMutableDictionary *subviews = nil; for (TVCMainWindowChannelViewSubview *subview in self.subviews) { NSString *uniqueIdentifier = subview.uniqueIdentifier; if (subviews == nil) { subviews = [NSMutableDictionary dictionary]; } subviews[uniqueIdentifier] = subview; } /* Once selectedItems is processed, the value of subviewsUnclaimed will be subviews that are no longer selected */ NSMutableDictionary *subviewsUnclaimed = nil; if (subviews) { subviewsUnclaimed = [subviews mutableCopy]; } /* Enumerate views that the user has selected */ IRCTreeItem *itemSelected = mainWindow.selectedItem; __block NSUInteger itemSelectedIndex = NSNotFound; [selectedItems enumerateObjectsUsingBlock:^(IRCTreeItem *item, NSUInteger index, BOOL *stop) { NSString *uniqueIdentifier = item.uniqueIdentifier; TVCMainWindowChannelViewSubview *subview = nil; BOOL subviewIsNew = YES; if (subviews) { subview = subviews[uniqueIdentifier]; if (subview) { subviewIsNew = NO; [subviewsUnclaimed removeObjectForKey:uniqueIdentifier]; } } if (subview == nil) { subview = [self subviewForItem:item]; } TVCLogView *backingView = [self backingViewForItem:item]; subview.backingView = backingView; subview.itemIndex = index; if (itemSelected == item) { itemSelectedIndex = index; subview.isSelected = YES; } else { subview.isSelected = NO; /* -isSelected is defaulted to NO which means for new views, -toggleOverlayView must be manually invoked because the setter wont change the value if they are same (NO == NO) */ if (subviewIsNew) { [subview toggleOverlayView]; } } subview.uniqueIdentifier = uniqueIdentifier; if (subviewIsNew) { [self addSubview:subview]; } }]; self.itemIndexSelected = itemSelectedIndex; /* Remove subviews that are no longer selected */ if (subviewsUnclaimed) { for (NSString *itemIdentifier in subviewsUnclaimed) { TVCMainWindowChannelViewSubview *subview = subviewsUnclaimed[itemIdentifier]; [subview removeFromSuperview]; } subviewsUnclaimed = nil; } /* Sort views */ if (subviews) { [self sortSubviewsUsingFunction:sortSubviews context:nil]; subviews = nil; } /* Size views */ [self adjustSubviews]; } - (void)selectionChangeTo:(NSUInteger)itemIndex { TVCMainWindow *mainWindow = self.mainWindow; NSArray *selectedItems = mainWindow.selectedItems; NSArray *subviews = self.subviews; NSUInteger itemIndexSelected = self.itemIndexSelected; if (itemIndexSelected != NSNotFound) { TVCMainWindowChannelViewSubview *oldItemView = subviews[itemIndexSelected]; oldItemView.isSelected = NO; [oldItemView toggleOverlayView]; } TVCMainWindowChannelViewSubview *newItemView = subviews[itemIndex]; newItemView.isSelected = YES; [newItemView toggleOverlayView]; self.itemIndexSelected = itemIndex; IRCTreeItem *newItem = selectedItems[itemIndex]; [mainWindow channelViewSelectionChangeTo:newItem]; } - (TVCLogView *)backingViewForItem:(IRCTreeItem *)item { return item.viewController.backingView; } - (TVCMainWindowChannelViewSubview *)subviewForItem:(IRCTreeItem *)item { NSRect splitViewFrame = self.frame; splitViewFrame.origin.x = 0.0; splitViewFrame.origin.y = 0.0; TVCMainWindowChannelViewSubview *overlayView = [[TVCMainWindowChannelViewSubview alloc] initWithFrame:splitViewFrame]; overlayView.parentView = self; return overlayView; } - (NSLayoutPriority)holdingPriorityForSubviewAtIndex:(NSInteger)subviewIndex { return 350.0; } - (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview { return NO; } - (CGFloat)dividerThickness { return 2.0; } - (NSColor *)dividerColor { return self.mainWindow.contentSplitView.dividerColor; } - (void)updateArrangement { TXChannelViewArrangement arrangement = [TPCPreferences channelViewArrangement]; self.vertical = (arrangement == TXChannelViewArrangedVertically); } - (void)themeAppearanceChanged:(NSNotification *)notification { [self updateVibrancy]; } - (void)updateVibrancy { if (theme().appearance == TPCThemeAppearanceTypeDark || themeSettings().underlyingWindowColorIsDark) { self.appearance = [TXAppearancePropertyCollection appKitDarkAppearance]; } else { self.appearance = [TXAppearancePropertyCollection appKitLightAppearance]; } } - (BOOL)needsDisplayWhenApplicationAppearanceChanges { return YES; } @end #pragma mark - #pragma mark Overlay View @implementation TVCMainWindowChannelViewSubview - (instancetype)initWithFrame:(NSRect)frameRect { if ((self = [super initWithFrame:frameRect])) { [self prepareInitialState]; return self; } return nil; } - (void)dealloc { [self teardownBackingView]; } - (void)prepareInitialState { self.translatesAutoresizingMaskIntoConstraints = NO; } - (BOOL)backingViewIsLoading { return self.backingView.isLayingOutView; } - (void)setIsSelected:(BOOL)isSelected { if (self->_isSelected != isSelected) { self->_isSelected = isSelected; [self toggleOverlayView]; } } - (void)setBackingView:(nullable TVCLogView *)backingView { if (self->_backingView != backingView) { [self teardownBackingView]; self->_backingView = backingView; [self setupWebView]; } } - (void)teardownBackingView { if (self.isObservingBackingView == NO) { return; } [self.backingView removeObserver:self forKeyPath:@"layingOutView"]; self.isObservingBackingView = NO; } - (void)setupWebView { TVCLogView *backingView = self.backingView; if (backingView == nil) { return; } if (self.backingViewIsLoading) { self.isObservingBackingView = YES; [backingView addObserver:self forKeyPath:@"layingOutView" options:NSKeyValueObservingOptionNew context:NULL]; } NSView *webView = backingView.webView; if (self.overlayVisible) { [self addSubview:webView positioned:NSWindowBelow relativeTo:self.overlayView]; } else { [self addSubview:webView]; } [self addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[webView(>=30)]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(webView)]]; [self addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[webView(>=30)]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(webView)]]; } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:@"layingOutView"]) { [self toggleOverlayView]; } } - (void)constructOverlayView { TVCMainWindowChannelViewSubviewOverlayView *overlayView = [[TVCMainWindowChannelViewSubviewOverlayView alloc] initWithFrame:self.frame]; overlayView.translatesAutoresizingMaskIntoConstraints = NO; self.overlayView = overlayView; } - (void)addOverlayView { if (self.overlayVisible) { [self.overlayView setNeedsDisplay:YES]; return; } if (self.overlayView == nil) { [self constructOverlayView]; } TVCMainWindowChannelViewSubviewOverlayView *overlayView = self.overlayView; [self addSubview:overlayView]; [self addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[overlayView]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(overlayView)]]; [self addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[overlayView]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(overlayView)]]; self.overlayVisible = YES; } - (void)toggleOverlayView { if (self.backingViewIsLoading || self.isSelected == NO) { [self addOverlayView]; } else { if ( self.overlayView) { [self.overlayView removeFromSuperview]; self.overlayVisible = NO; } } } - (void)mouseDownSelectionChange { if (self.backingViewIsLoading) { return; } [self.parentView selectionChangeTo:self.itemIndex]; } - (void)mouseDown:(NSEvent *)theEvent { if (self.overlayVisible) { [self mouseDownSelectionChange]; return; } [super mouseDown:theEvent]; } - (void)rightMouseDown:(NSEvent *)theEvent { if (self.overlayVisible) { [self mouseDownSelectionChange]; return; } [super rightMouseDown:theEvent]; } - (void)otherMouseDown:(NSEvent *)theEvent { if (self.overlayVisible) { [self mouseDownSelectionChange]; return; } [super otherMouseDown:theEvent]; } - (nullable NSView *)hitTest:(NSPoint)aPoint { if (NSPointInRect(aPoint, self.frame) == NO) { return nil; } if (self.overlayVisible) { return self.overlayView; } return [super hitTest:aPoint]; } @end #pragma mark - @implementation TVCMainWindowChannelViewSubviewOverlayView - (void)mouseDown:(NSEvent *)theEvent { [self.superview mouseDown:theEvent]; } - (void)rightMouseDown:(NSEvent *)theEvent { [self.superview rightMouseDown:theEvent]; } - (void)otherMouseDown:(NSEvent *)theEvent { [self.superview otherMouseDown:theEvent]; } - (void)drawRect:(NSRect)dirtyRect { if ([self needsToDrawRect:dirtyRect] == NO) { return; } TVCMainWindowChannelViewSubview *subview = (id)self.superview; NSColor *backgroundColor = nil; if (subview.backingViewIsLoading) { backgroundColor = themeSettings().underlyingWindowColor; } else { backgroundColor = themeSettings().channelViewOverlayColor; } if (backgroundColor == nil) { backgroundColor = [self defaultBackgroundColor]; } [backgroundColor set]; [NSBezierPath fillRect:dirtyRect]; } - (NSColor *)defaultBackgroundColor { TVCMainWindow *mainWindow = self.mainWindow; TVCMainWindowAppearance *appearance = mainWindow.userInterfaceObjects; if (appearance == nil) { return [NSColor blackColor]; } if (mainWindow.isActiveForDrawing) { return appearance.channelViewOverlayDefaultBackgroundColorActiveWindow; } else { return appearance.channelViewOverlayDefaultBackgroundColorInactiveWindow; } } - (nullable NSView *)hitTest:(NSPoint)aPoint { return self; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindowLoadingScreen.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSViewHelperPrivate.h" #import "TVCMainWindowSplitView.h" #import "TVCMainWindowTextViewPrivate.h" #import "TVCMainWindowPrivate.h" #import "TVCMainWindowLoadingScreenPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowLoadingScreenView () @property (nonatomic, weak) NSView *visibleView; @property (nonatomic, strong) IBOutlet NSView *welcomeAddServerView; @property (nonatomic, weak) IBOutlet NSButton *welcomeAddServerViewContinueButton; @property (nonatomic, strong) IBOutlet NSView *progressView; @property (nonatomic, weak) IBOutlet NSTextField *progressViewDescriptionTextField; @property (nonatomic, weak) IBOutlet NSProgressIndicator *progressViewIndicator; @property (nonatomic, strong) IBOutlet NSView *trialExpiredView; @end @implementation TVCMainWindowLoadingScreenView #pragma mark - #pragma mark Display View (Public) - (void)showWelcomeAddServerView { [self displayView:self.welcomeAddServerView]; } - (void)showProgressViewWithReason:(NSString *)progressReason { NSParameterAssert(progressReason != nil); [self displayView:self.progressView]; [self setProgressViewReason:progressReason]; [self.progressViewIndicator startAnimation:nil]; } - (void)setProgressViewReason:(NSString *)progressReason { NSParameterAssert(progressReason != nil); self.progressViewDescriptionTextField.stringValue = progressReason; } - (void)showTrialExpiredView { [self displayView:self.trialExpiredView]; } #pragma mark - - (void)hide { [self hideAnimated:NO]; } - (void)hideAnimated { [self hideAnimated:YES]; } - (void)hideAnimated:(BOOL)animated { [self hideView:self.visibleView animate:animated]; } #pragma mark - #pragma mark Display View (Private) - (void)displayView:(NSView *)view { NSParameterAssert(view != nil); if (self.visibleView != nil) { [self.visibleView removeFromSuperview]; } self.visibleView = view; [self disableBackgroundControlsStepOne]; [self addSubview:view]; NSArray *constraints = @[ /* Center view on both axis. */ [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0], /* Constrain edges but use >= so that window does not shrink to conform to it. */ [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0], ]; [self addConstraints:constraints]; self.alphaValue = 1.0; self.hidden = NO; [self displayIfNeeded]; [self disableBackgroundControlsStepTwo]; } #pragma mark - #pragma mark Hide View (Private) - (void)hideView:(NSView *)view animate:(BOOL)animate { [self enableBackgroundControlsStepOne]; /* ================================== */ void (^phaseTwoBlock)(NSView *) = ^(NSView *viewToHide) { [view removeFromSuperview]; /* We only continue with phase two if we have not replaced the visible view with a new one. */ if (self.visibleView == viewToHide) { self.visibleView = nil; self.hidden = YES; [self enableBackgroundControlsStepTwo]; } }; /* ================================== */ if (animate == NO) { self.alphaValue = 0.0; phaseTwoBlock(view); return; } /* ================================== */ RZAnimationCurrentContext().duration = 1.0; [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { self.animator.alphaValue = 0.0; } completionHandler:^{ phaseTwoBlock(view); }]; } #pragma mark - #pragma mark Private Utilities - (BOOL)viewIsVisible { return (self.isHidden == NO); } - (void)disableBackgroundControlsStepOne { self.mainWindow.contentSplitView.hidden = YES; } - (void)disableBackgroundControlsStepTwo { TVCMainWindowTextView *textField = self.mainWindow.inputTextField; textField.editable = NO; textField.selectable = NO; [textField updateSegmentedController]; } - (void)enableBackgroundControlsStepOne { self.mainWindow.contentSplitView.hidden = NO; } - (void)enableBackgroundControlsStepTwo { TVCMainWindowTextView *textField = self.mainWindow.inputTextField; textField.editable = YES; textField.selectable = YES; [textField updateSegmentedController]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindowSegmentedControl.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSViewHelperPrivate.h" #import "TXMasterController.h" #import "TXMenuController.h" #import "TPCPreferencesLocal.h" #import "TVCMainWindow.h" #import "TVCMainWindowLoadingScreen.h" #import "IRCClient.h" #import "IRCWorld.h" #import "TVCMainWindowSegmentedControlPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowSegmentedController () @property (nonatomic, weak) IBOutlet NSLayoutConstraint *segmentedControllerLeadingConstraint; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *segmentedControllerWidthConstraint; @end @implementation TVCMainWindowSegmentedController - (void)segmentedCellClicked:(id)sender { NSInteger selectedSegment = self.selectedSegment; if (selectedSegment == 2) { [menuController() showAddressBook:sender]; } } - (void)updateSegmentedControllerOrigin { if ([TPCPreferences hideMainWindowSegmentedController]) { [self.segmentedControllerLeadingConstraint archiveConstantAndZeroOut]; [self.segmentedControllerWidthConstraint archiveConstantAndZeroOut]; } else { [self.segmentedControllerLeadingConstraint restoreArchivedConstant]; [self.segmentedControllerWidthConstraint restoreArchivedConstant]; } } - (void)updateSegmentedController { if ([TPCPreferences hideMainWindowSegmentedController]) { return; } TVCMainWindow *mainWindow = self.mainWindow; IRCClient *selectedClient = mainWindow.selectedClient; IRCChannel *selectedChannel = mainWindow.selectedChannel; /* Enable controller */ self.enabled = (worldController().clientCount > 0 && mainWindow.loadingScreen.viewIsVisible == NO); // Cell 0 NSMenu *cell0Menu = menuController().mainWindowSegmentedControllerCellMenu; [self setMenu:cell0Menu forSegment:0]; // Cell 1 NSMenuItem *cell1MenuItem = nil; if (selectedChannel == nil) { cell1MenuItem = menuController().mainMenuServerMenuItem; } else { cell1MenuItem = menuController().mainMenuChannelMenuItem; } [self setMenu:cell1MenuItem.submenu forSegment:1]; // Cell 2 [self setEnabled:(selectedClient.isConnected) forSegment:2]; } @end #pragma mark - @implementation TVCMainWindowSegmentedControllerCell - (nullable SEL)action { NSInteger selectedSegment = self.selectedSegment; if ([self menuForSegment:selectedSegment] == nil) { return super.action; } else { return nil; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindowSplitView.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSViewHelperPrivate.h" #import "TPCPreferencesLocal.h" #import "TPCPreferencesUserDefaults.h" #import "TVCServerList.h" #import "TVCServerListAppearance.h" #import "TVCMemberList.h" #import "TVCMemberListAppearance.h" #import "TVCMainWindow.h" #import "TVCMainWindowAppearance.h" #import "TVCMainWindowSplitViewPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const _userDefaultsKey = @"Window -> Main Window -> Split Channel View Saved Frames"; @interface TVCMainWindowSplitView () @property (nonatomic, assign) BOOL restoredPositions; @property (nonatomic, assign) BOOL stopFrameUpdatesForServerList; @property (nonatomic, assign) BOOL stopFrameUpdatesForMemberList; @property (nonatomic, strong) IBOutlet NSLayoutConstraint *serverListWidthMinConstraint; @property (nonatomic, strong) IBOutlet NSLayoutConstraint *serverListWidthMaxConstraint; @property (nonatomic, strong) IBOutlet NSLayoutConstraint *memberListWidthMinConstraint; @property (nonatomic, strong) IBOutlet NSLayoutConstraint *memberListWidthMaxConstraint; @end @implementation TVCMainWindowSplitView - (void)applicationAppearanceChanged { TVCMainWindowAppearance *appearance = self.mainWindow.userInterfaceObjects; TVCServerListAppearance *serverList = appearance.serverList; self.serverListWidthMinConstraint.constant = serverList.minimumWidth; self.serverListWidthMaxConstraint.constant = serverList.maximumWidth; TVCMemberListAppearance *memberList = appearance.memberList; self.memberListWidthMinConstraint.constant = memberList.minimumWidth; self.memberListWidthMaxConstraint.constant = memberList.maximumWidth; } - (NSColor *)dividerColor { TVCMainWindowAppearance *appearance = self.mainWindow.userInterfaceObjects; NSColor *dividerColor = appearance.splitViewDividerColor; if (appearance.isDarkAppearance) { dividerColor = dividerColor.invertedColor; } return dividerColor; } - (void)awakeFromNib { [super awakeFromNib]; self.delegate = (id)self; } - (void)restorePositions { if (self.restoredPositions) { return; } [self expandMemberList]; [self expandServerList]; // Set after expanding views so that -resizeWithOldSuperviewSize: // does not save the views' frames prematurely self.restoredPositions = YES; } - (NSRect)splitView:(NSSplitView *)splitView effectiveRect:(NSRect)proposedEffectiveRect forDrawnRect:(NSRect)drawnRect ofDividerAtIndex:(NSInteger)dividerIndex { if (dividerIndex == 0) { if (self.serverListCollapsed) { return NSZeroRect; } } else if (dividerIndex == 1) { if (self.memberListCollapsed) { return NSZeroRect; } } return proposedEffectiveRect; } - (void)splitViewDidResizeSubviews:(NSNotification *)notification { if (self.restoredPositions) { [self updateSavedFrames]; } } - (BOOL)allowsVibrancy { return YES; } - (void)expandServerList { NSScrollView *scrollView = self.mainWindow.serverList.enclosingScrollView; scrollView.hasVerticalScroller = YES; NSView *subview = self.subviews[0]; subview.hidden = NO; [self setPosition:[self positionToRestoreServerListAt] ofDividerAtIndex:0]; self.serverListWidthMinConstraint.active = YES; self.stopFrameUpdatesForServerList = NO; } - (void)expandMemberList { NSScrollView *scrollView = self.mainWindow.memberList.enclosingScrollView; scrollView.hasVerticalScroller = YES; NSView *subview = self.subviews[2]; subview.hidden = NO; [self setPosition:[self positionToRestoreMemberListAt] ofDividerAtIndex:1]; self.memberListWidthMinConstraint.active = YES; self.stopFrameUpdatesForMemberList = NO; } - (void)collapseServerList { self.stopFrameUpdatesForServerList = YES; self.serverListWidthMinConstraint.active = NO; NSScrollView *scrollView = self.mainWindow.serverList.enclosingScrollView; scrollView.hasVerticalScroller = NO; NSView *subview = self.subviews[0]; [self setPosition:0.0 ofDividerAtIndex:0]; subview.hidden = YES; } - (void)collapseMemberList { self.stopFrameUpdatesForMemberList = YES; self.memberListWidthMinConstraint.active = NO; NSView *subview = self.subviews[2]; TVCMainWindow *mainWindow = self.mainWindow; NSScrollView *scrollView = mainWindow.memberList.enclosingScrollView; scrollView.hasVerticalScroller = NO; NSRect windowFrame = mainWindow.frame; [self setPosition:NSWidth(windowFrame) ofDividerAtIndex:1]; subview.hidden = YES; } - (void)toggleServerListVisibility { if (self.serverListCollapsed) { [self expandServerList]; } else { [self collapseServerList]; } } - (void)toggleMemberListVisibility { if (self.memberListCollapsed) { [self expandMemberList]; } else { [self collapseMemberList]; } } - (CGFloat)positionForDividerAtIndex:(NSInteger)index { NSRect subviewFrame = self.subviews[index].frame; return (NSMaxX(subviewFrame) + (self.dividerThickness * index)); } - (CGFloat)positionToRestoreServerListAt { NSDictionary *frames = [self savedFrames]; CGFloat position = [frames doubleForKey:@"serverList"]; TVCServerListAppearance *appearance = self.mainWindow.userInterfaceObjects.serverList; if (position < appearance.minimumWidth) { position = appearance.defaultWidth; } else if (position > appearance.maximumWidth) { position = appearance.defaultWidth; } return position; } - (CGFloat)positionToRestoreMemberListAt { return [self positionToRestoreMemberListAt:YES]; } - (CGFloat)positionToRestoreMemberListAt:(BOOL)correctedFrame { NSDictionary *frames = [self savedFrames]; CGFloat position = [frames doubleForKey:@"memberList"]; TVCMemberListAppearance *appearance = self.mainWindow.userInterfaceObjects.memberList; if (position < appearance.minimumWidth) { position = appearance.defaultWidth; } else if (position > appearance.maximumWidth) { position = appearance.defaultWidth; } if (correctedFrame) { NSRect windowFrame = self.mainWindow.frame; return ((NSWidth(windowFrame) - position) - self.dividerThickness); } return position; } - (CGFloat)positionOfServerListForSaving { CGFloat position = [self positionForDividerAtIndex:0]; TVCServerListAppearance *appearance = self.mainWindow.userInterfaceObjects.serverList; if (position < appearance.minimumWidth) { position = appearance.defaultWidth; } else if (position > appearance.maximumWidth) { position = appearance.defaultWidth; } return position; } - (CGFloat)positionOfMemberListForSaving { CGFloat position = [self positionForDividerAtIndex:1]; NSRect windowFrame = self.mainWindow.frame; position = ((position - NSWidth(windowFrame)) * (-1)); TVCMemberListAppearance *appearance = self.mainWindow.userInterfaceObjects.memberList; if (position < appearance.minimumWidth) { position = appearance.defaultWidth; } else if (position > appearance.maximumWidth) { position = appearance.defaultWidth; } return position; } - (void)updateSavedFrames { CGFloat serverListPosition = 0; if (self.stopFrameUpdatesForServerList) { serverListPosition = [self positionToRestoreServerListAt]; } else { serverListPosition = [self positionOfServerListForSaving]; } CGFloat memberListPosition = 0; if (self.stopFrameUpdatesForMemberList) { memberListPosition = [self positionToRestoreMemberListAt:NO]; } else { memberListPosition = [self positionOfMemberListForSaving]; } NSDictionary *newFrames = @{ @"serverList" : @(serverListPosition), @"memberList" : @(memberListPosition), }; [RZUserDefaults() setObject:newFrames forKey:_userDefaultsKey]; } - (nullable NSDictionary *)savedFrames { return [RZUserDefaults() objectForKey:_userDefaultsKey]; } - (BOOL)isServerListCollapsed { return [self isSubviewCollapsed:self.subviews[0]]; } - (BOOL)isMemberListCollapsed { return [self isSubviewCollapsed:self.subviews[2]]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindowTextView.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSViewHelperPrivate.h" #import "IRCColorFormat.h" #import "TLOLocalization.h" #import "TPCResourceManagerPrivate.h" #import "TPCPreferencesLocalPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TVCMainWindow.h" #import "TVCMainWindowSegmentedControlPrivate.h" #import "TVCTextViewWithIRCFormatterPrivate.h" #import "TVCMainWindowTextViewAppearancePrivate.h" #import "TVCMainWindowTextViewPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _KeyObservingArray @[ @"TextFieldAutomaticSpellCheck", \ @"TextFieldAutomaticGrammarCheck", \ @"TextFieldAutomaticSpellCorrection", \ @"TextFieldSmartCopyPaste", \ @"TextFieldSmartQuotes", \ @"TextFieldSmartDashes", \ @"TextFieldSmartLinks", \ @"TextFieldDataDetectors", \ @"TextFieldTextReplacement"] @interface TVCMainWindowTextView () @property (nonatomic, copy) NSAttributedString *placeholderAttributedString; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *textViewHeightConstraint; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *windowContentViewMinimumHeight; @property (nonatomic, weak) IBOutlet TVCMainWindowTextViewBackground *backgroundView; @property (nonatomic, weak) IBOutlet TVCMainWindowTextViewContentView *contentView; @property (nonatomic, weak) IBOutlet TVCMainWindowSegmentedController *segmentedController; @property (nonatomic, weak) IBOutlet TVCMainWindowSegmentedControllerCell *segmentedControllerCell; @property (nonatomic, strong) TVCMainWindowTextViewAppearance *userInterfaceObjects; @property (readonly) NSArray *defaultSpellingIgnores; @end @interface TVCMainWindowTextViewBackground () @property (nonatomic, unsafe_unretained) IBOutlet TVCMainWindowTextView *textView; @end @interface TVCMainWindowTextViewContentView () @property (nonatomic, unsafe_unretained) IBOutlet TVCMainWindowTextView *textView; @property (nonatomic, weak) IBOutlet TVCMainWindowSegmentedController *segmentedController; @end @implementation TVCMainWindowTextView #pragma mark - #pragma mark Drawing - (void)awakeFromNib { [super awakeFromNib]; self.backgroundColor = [NSColor clearColor]; [self updateTextDirection]; } - (void)viewDidMoveToWindow { [super viewDidMoveToWindow]; NSWindow *window = self.window; if (window) { for (NSString *key in _KeyObservingArray) { [RZUserDefaults() addObserver:self forKeyPath:key options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:NULL]; } } else // window { for (NSString *key in _KeyObservingArray) { [RZUserDefaults() removeObserver:self forKeyPath:key]; } } } - (void)updateVibrancyWithAppearance:(TVCMainWindowTextViewAppearance *)appearance { NSParameterAssert(appearance != nil); self.backgroundView.needsDisplay = YES; self.contentView.needsDisplay = YES; } - (void)applicationAppearanceChanged { TVCMainWindowTextViewAppearance *appearance = self.mainWindow.userInterfaceObjects.textView; [self _updateAppearance:appearance]; } /* - (void)systemAppearanceChanged { } */ - (void)_updateAppearance:(TVCMainWindowTextViewAppearance *)appearance { NSParameterAssert(appearance != nil); self.userInterfaceObjects = appearance; [self updateVibrancyWithAppearance:appearance]; self.textContainerInset = appearance.textViewInset; self.preferredFontColor = appearance.textViewTextColor; [self reloadOriginPoints]; [self updateTextBoxCachedPreferredFontSize]; [self resetTypeSetterAttributes]; [self updateAllFontColorsToMatchTheDefaultFont]; } #pragma mark - #pragma mark Segmented Controller - (void)reloadOriginPointsAndRecalculateSize { [self reloadOriginPoints]; /* Reload size on next go around to allow constraint layout to occur before so */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [self recalculateTextViewSizeForced]; }); } - (void)reloadOriginPoints { [self.segmentedController updateSegmentedControllerOrigin]; } - (void)updateSegmentedController { [self.segmentedController updateSegmentedController]; } #pragma mark - #pragma mark Spelling - (void)resetSpellingIgnores { /* When performing nickname completion, completions are added to the ignore list. The main window then resets the entire spelling ignore list, by calling this method when the selection changes. */ /* Because the main window will eventually call this for us, we allow it to happen lazily, instead of in awake from nib. */ [RZSpellChecker() setIgnoredWords:self.defaultSpellingIgnores inSpellDocumentWithTag:self.spellCheckerDocumentTag]; } - (NSArray *)defaultSpellingIgnores { return [TPCResourceManager arrayFromResources:@"StaticStore" key:@"Spelling Ignores"]; } #pragma mark - #pragma mark Utilities - (void)updateAllFontColorsToMatchTheDefaultFont { [self.textStorage beginEditing]; [self.textStorage enumerateAttributesInRange:self.range options:0 usingBlock:^(NSDictionary *attributes, NSRange effectiveRange, BOOL *stop) { if ([attributes containsKey:IRCTextFormatterForegroundColorAttributeName]) { return; } [self resetFontColorInRange:effectiveRange]; }]; [self.textStorage endEditing]; } - (void)setAttributedStringValue:(NSAttributedString *)attributedStringValue { super.attributedStringValue = attributedStringValue; [self updateAllFontColorsToMatchTheDefaultFont]; } - (void)updateTextDirection { if ([TPCPreferences rightToLeftFormatting]) { self.baseWritingDirection = NSWritingDirectionRightToLeft; } else { self.baseWritingDirection = NSWritingDirectionLeftToRight; } } - (void)textDidChange:(NSNotification *)aNotification { [super textDidChange:aNotification]; [self recalculateTextViewSize]; } - (void)paste:(nullable id)sender { [super paste:self]; [self recalculateTextViewSize]; } - (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector { if (aSelector == @selector(insertNewline:)) { [self.mainWindow textEntered]; return YES; } return NO; } #pragma mark - #pragma mark Multi-line Text Box Drawing - (void)drawRect:(NSRect)dirtyRect { if ([self needsToDrawRect:dirtyRect] == NO) { return; } /* The place holder string is not drawn for right to left users */ if (self.stringLength > 0 || self.baseWritingDirection != NSWritingDirectionLeftToRight) { [super drawRect:dirtyRect]; return; } // TODO: Don't used fix positions NSRect selectedRect = self.selectedRect; TVCMainWindowTextViewFontSize preferredFontSize = self.userInterfaceObjects.textViewPreferredFontSize; if (preferredFontSize == TVCMainWindowTextViewFontSizeNormal || preferredFontSize == TVCMainWindowTextViewFontSizeLarge) { selectedRect.origin.y -= 1; } [self.placeholderAttributedString drawAtPoint:selectedRect.origin]; } - (void)updateTextBoxCachedPreferredFontSize { /* Update font */ TVCMainWindowTextViewAppearance *appearance = self.userInterfaceObjects; if ([appearance preferredTextViewFontChanged] == NO) { return; } NSFont *preferredFont = appearance.textViewPreferredFont; self.preferredFont = preferredFont; /* Update the placeholder string */ NSColor *placeholderTextColor = appearance.textViewPlaceholderTextColor; NSDictionary *placeholderStringAttributes = @{ NSFontAttributeName : preferredFont, NSForegroundColorAttributeName : placeholderTextColor }; self.placeholderAttributedString = [NSAttributedString attributedStringWithString:TXTLS(@"TVCMainWindow[8r3-ih]") attributes:placeholderStringAttributes]; self.needsDisplay = YES; } - (void)updateTextBasedOnPreferredFontSize { TVCMainWindowTextViewAppearance *appearance = self.userInterfaceObjects; TVCMainWindowTextViewFontSize preferredFontSize = appearance.textViewPreferredFontSize; [self updateTextBoxCachedPreferredFontSize]; if (appearance.textViewPreferredFontSize != preferredFontSize) { [self updateAllFontSizesToMatchTheDefaultFont]; } [self recalculateTextViewSizeForced]; } - (CGFloat)defaultLineHeight { return [self.layoutManager defaultLineHeightForFont:self.preferredFont]; } - (void)recalculateTextViewSize { [self recalculateTextViewSizeForced:NO]; } - (void)recalculateTextViewSizeForced { [self recalculateTextViewSizeForced:YES]; } - (void)recalculateTextViewSizeForced:(BOOL)forceRecalculate { TVCMainWindowTextViewAppearance *appearance = self.userInterfaceObjects; NSWindow *window = self.window; NSRect windowFrame = window.frame; CGFloat contentBorderPadding = appearance.backgroundViewContentBorderPadding; CGFloat backgroundHeight = 0; CGFloat backgroundHeightDefault = [self defaultLineHeight]; if (self.stringLength < 1) { backgroundHeight = (backgroundHeightDefault + contentBorderPadding); } else { CGFloat backgroundHeightMaximum = (NSHeight(windowFrame) - (self.windowContentViewMinimumHeight.constant + contentBorderPadding)); backgroundHeight = [self highestHeightBelowHeight:backgroundHeightMaximum withPadding:contentBorderPadding]; if ((backgroundHeight - contentBorderPadding) < backgroundHeightDefault) { backgroundHeight = (backgroundHeightDefault + contentBorderPadding); } } self.textViewHeightConstraint.constant = backgroundHeight; id scrollViewContentView = self.enclosingScrollView.contentView; NSRect contentViewBounds = [scrollViewContentView bounds]; if (contentViewBounds.origin.x > 0) { contentViewBounds.origin.x = 0; [scrollViewContentView scrollToPoint:contentViewBounds.origin]; } } #pragma mark - #pragma mark NSTextView Context Menu Preferences - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:@"TextFieldAutomaticSpellCheck"]) { self.continuousSpellCheckingEnabled = [TPCPreferences textFieldAutomaticSpellCheck]; } else if ([keyPath isEqualToString:@"TextFieldAutomaticGrammarCheck"]) { self.grammarCheckingEnabled = [TPCPreferences textFieldAutomaticGrammarCheck]; } else if ([keyPath isEqualToString:@"TextFieldAutomaticSpellCorrection"]) { self.automaticSpellingCorrectionEnabled = [TPCPreferences textFieldAutomaticSpellCorrection]; } else if ([keyPath isEqualToString:@"TextFieldSmartCopyPaste"]) { self.smartInsertDeleteEnabled = [TPCPreferences textFieldSmartCopyPaste]; } else if ([keyPath isEqualToString:@"TextFieldSmartQuotes"]) { self.automaticQuoteSubstitutionEnabled = [TPCPreferences textFieldSmartQuotes]; } else if ([keyPath isEqualToString:@"TextFieldSmartDashes"]) { self.automaticDashSubstitutionEnabled = [TPCPreferences textFieldSmartDashes]; } else if ([keyPath isEqualToString:@"TextFieldSmartLinks"]) { self.automaticLinkDetectionEnabled = [TPCPreferences textFieldSmartLinks]; } else if ([keyPath isEqualToString:@"TextFieldDataDetectors"]) { self.automaticDataDetectionEnabled = [TPCPreferences textFieldDataDetectors]; } else if ([keyPath isEqualToString:@"TextFieldTextReplacement"]) { self.automaticTextReplacementEnabled = [TPCPreferences textFieldTextReplacement]; } else if ([super respondsToSelector:@selector(observeValueForKeyPath:ofObject:change:context:)]) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)setContinuousSpellCheckingEnabled:(BOOL)continuousSpellCheckingEnabled { [TPCPreferences setTextFieldAutomaticSpellCheck:continuousSpellCheckingEnabled]; super.continuousSpellCheckingEnabled = continuousSpellCheckingEnabled; } - (void)setGrammarCheckingEnabled:(BOOL)grammarCheckingEnabled { [TPCPreferences setTextFieldAutomaticGrammarCheck:grammarCheckingEnabled]; super.grammarCheckingEnabled = grammarCheckingEnabled; } - (void)setAutomaticSpellingCorrectionEnabled:(BOOL)automaticSpellingCorrectionEnabled { [TPCPreferences setTextFieldAutomaticSpellCorrection:automaticSpellingCorrectionEnabled]; super.automaticSpellingCorrectionEnabled = automaticSpellingCorrectionEnabled; } - (void)setSmartInsertDeleteEnabled:(BOOL)smartInsertDeleteEnabled { [TPCPreferences setTextFieldSmartCopyPaste:smartInsertDeleteEnabled]; super.smartInsertDeleteEnabled = smartInsertDeleteEnabled; } - (void)setAutomaticQuoteSubstitutionEnabled:(BOOL)automaticQuoteSubstitutionEnabled { [TPCPreferences setTextFieldSmartQuotes:automaticQuoteSubstitutionEnabled]; super.automaticQuoteSubstitutionEnabled = automaticQuoteSubstitutionEnabled; } - (void)setAutomaticDashSubstitutionEnabled:(BOOL)automaticDashSubstitutionEnabled { [TPCPreferences setTextFieldSmartDashes:automaticDashSubstitutionEnabled]; super.automaticDashSubstitutionEnabled = automaticDashSubstitutionEnabled; } - (void)setAutomaticLinkDetectionEnabled:(BOOL)automaticLinkDetectionEnabled { [TPCPreferences setTextFieldSmartLinks:automaticLinkDetectionEnabled]; super.automaticLinkDetectionEnabled = automaticLinkDetectionEnabled; } - (void)setAutomaticDataDetectionEnabled:(BOOL)automaticDataDetectionEnabled { [TPCPreferences setTextFieldDataDetectors:automaticDataDetectionEnabled]; super.automaticDataDetectionEnabled = automaticDataDetectionEnabled; } - (void)setAutomaticTextReplacementEnabled:(BOOL)automaticTextReplacementEnabled { [TPCPreferences setTextFieldTextReplacement:automaticTextReplacementEnabled]; super.automaticTextReplacementEnabled = automaticTextReplacementEnabled; } @end #pragma mark - #pragma mark Background Drawing @implementation TVCMainWindowTextViewBackground - (void)drawControllerForWithAppearance:(TVCMainWindowTextViewAppearance *)appearance { NSParameterAssert(appearance != nil); if (appearance.isDarkAppearance == NO) { [self drawLightControllerForWithAppearance:appearance]; } else { [self drawDarkControllerForWithAppearance:appearance]; } } - (void)drawDarkControllerForWithAppearance:(TVCMainWindowTextViewAppearance *)appearance { NSParameterAssert(appearance != nil); BOOL isWindowActive = self.mainWindow.activeForDrawing; NSRect cellBounds = self.frame; NSRect controlFrame = NSMakeRect(0.0, 1.0, cellBounds.size.width, (cellBounds.size.height - 2.0)); /* Inner background color */ NSColor *backgroundColor = nil; if (isWindowActive) { backgroundColor = appearance.textViewBackgroundColorActiveWindow; } else { backgroundColor = appearance.textViewBackgroundColorInactiveWindow; } // isWindowActive /* Shadow colors */ NSShadow *outsideShadow = [NSShadow new]; outsideShadow.shadowBlurRadius = 0.0; outsideShadow.shadowOffset = NSMakeSize(0.0, (-1.0)); if (isWindowActive) { outsideShadow.shadowColor = appearance.textViewOutsidePrimaryShadowColorActiveWindow; } else { outsideShadow.shadowColor = appearance.textViewOutsidePrimaryShadowColorInactiveWindow; } // isWindowActive /* Rectangle drawing */ NSBezierPath *rectanglePath = [NSBezierPath bezierPathWithRoundedRect:controlFrame xRadius:3.0 yRadius:3.0]; [NSGraphicsContext saveGraphicsState]; [outsideShadow set]; [backgroundColor setFill]; [rectanglePath fill]; [NSGraphicsContext restoreGraphicsState]; } - (void)drawLightControllerForWithAppearance:(TVCMainWindowTextViewAppearance *)appearance { NSParameterAssert(appearance != nil); /* To be honest, I don't remember what any of this does. */ BOOL isWindowActive = self.mainWindow.activeForDrawing; NSRect cellBounds = self.frame; NSRect controlFrame = NSMakeRect(0.0, 1.0, cellBounds.size.width, (cellBounds.size.height - 2.0)); CGContextRef context = RZGraphicsCurrentContext().CGContext; /* Inner gradient color */ NSGradient *insideGradient = nil; if (isWindowActive) { insideGradient = appearance.textViewInsideGradientActiveWindow; } else { insideGradient = appearance.textViewInsideGradientInactiveWindow; } // isWindowActive /* Inside shadow */ NSShadow *insideShadow = [NSShadow new]; insideShadow.shadowBlurRadius = 0.0; insideShadow.shadowOffset = NSMakeSize(0.0, (-1.0)); NSColor *insideShadowColor = nil; if (isWindowActive) { insideShadowColor = appearance.textViewInsideShadowColorActiveWindow; } else { insideShadowColor = appearance.textViewInsideShadowColorInactiveWindow; } insideShadow.shadowColor = insideShadowColor; /* Outside shadow */ NSShadow *outsideShadow = [NSShadow new]; if (appearance.isHighResolutionAppearance == NO) { outsideShadow.shadowBlurRadius = 0.0; outsideShadow.shadowOffset = NSMakeSize(0.0, (-1.0)); } else { outsideShadow.shadowBlurRadius = 0.0; outsideShadow.shadowOffset = NSMakeSize(0.0, (-0.5)); } // high resolution if (isWindowActive) { outsideShadow.shadowColor = appearance.textViewOutsidePrimaryShadowColorActiveWindow; } else { outsideShadow.shadowColor = appearance.textViewOutsidePrimaryShadowColorInactiveWindow; } // isWindowActive /* Rectangle drawing */ NSBezierPath *rectanglePath = [NSBezierPath bezierPathWithRoundedRect:controlFrame xRadius:3.0 yRadius:3.0]; [outsideShadow set]; CGContextBeginTransparencyLayer(context, NULL); [insideGradient drawInBezierPath:rectanglePath angle:(-90)]; CGContextEndTransparencyLayer(context); /* Prepare drawing for inside shadow */ CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL); CGContextSetAlpha(context, insideShadowColor.alphaComponent); CGContextBeginTransparencyLayer(context, NULL); { /* Inside shadow drawing */ [insideShadow set]; CGContextSetBlendMode(context, kCGBlendModeSourceOut); CGContextBeginTransparencyLayer(context, NULL); /* Fill shadow */ [insideShadowColor setFill]; [rectanglePath fill]; /* Complete drawing */ CGContextEndTransparencyLayer(context); } CGContextEndTransparencyLayer(context); /* On retina, we fake a second shadow under the bottommost one */ if (appearance.isHighResolutionAppearance) { NSColor *controlColor = nil; if (isWindowActive) { controlColor = appearance.textViewOutsideSecondaryShadowColorActiveWindow; } else { controlColor = appearance.textViewOutsideSecondaryShadowColorInactiveWindow; } // isWindowActive [controlColor setStroke]; NSPoint linePoint1 = NSMakePoint(2.0, 0.0); NSPoint linePoint2 = NSMakePoint((cellBounds.size.width - 2.0), 0.0); [NSBezierPath strokeLineFromPoint:linePoint1 toPoint:linePoint2]; } // high resolution } - (void)drawRect:(NSRect)dirtyRect { if ([self needsToDrawRect:dirtyRect] == NO) { return; } TVCMainWindowTextViewAppearance *appearance = self.textView.userInterfaceObjects; if (appearance == nil) { return; } [self drawControllerForWithAppearance:appearance]; } @end #pragma mark - #pragma mark Text Field Background Vibrant View @implementation TVCMainWindowTextViewContentView - (void)drawRect:(NSRect)dirtyRect { if ([self needsToDrawRect:dirtyRect] == NO) { return; } TVCMainWindowTextViewAppearance *appearance = self.textView.userInterfaceObjects; if (appearance == nil) { return; } /* Draw background color */ NSColor *backgroundColor = appearance.backgroundViewBackgroundColor; [backgroundColor set]; NSRectFill(dirtyRect); /* Draw divider */ NSRect contentViewFrame = self.frame; contentViewFrame.origin.x = 0.0; contentViewFrame.origin.y = (NSMaxY(contentViewFrame) - 1.0); contentViewFrame.size.height = 1.0; NSBezierPath *dividerPath = [NSBezierPath bezierPathWithRect:contentViewFrame]; NSColor *dividerColor = appearance.backgroundViewDividerColor; [dividerColor set]; [dividerPath fill]; } - (BOOL)allowsVibrancy { return NO; } - (BOOL)isOpaque { return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindowTextViewAppearance.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TVCMainWindow.h" #import "TVCAppearancePrivate.h" #import "TVCMainWindowTextViewAppearancePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMainWindowTextViewAppearance () #pragma mark - #pragma mark Text View @property (nonatomic, assign, readwrite) NSSize textViewInset; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewTextColor; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewPlaceholderTextColor; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewBackgroundColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewBackgroundColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewOutlineColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewOutlineColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewInsideShadowColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewInsideShadowColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSGradient *textViewInsideGradientActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSGradient *textViewInsideGradientInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewOutsidePrimaryShadowColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewOutsidePrimaryShadowColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewOutsideSecondaryShadowColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *textViewOutsideSecondaryShadowColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSFont *textViewFont; @property (nonatomic, copy, nullable, readwrite) NSFont *textViewFontLarge; @property (nonatomic, copy, nullable, readwrite) NSFont *textViewFontExtraLarge; @property (nonatomic, copy, nullable, readwrite) NSFont *textViewFontHumongous; @property (nonatomic, assign, readwrite) TVCMainWindowTextViewFontSize textViewPreferredFontSize; #pragma mark - #pragma mark Background View @property (nonatomic, copy, nullable, readwrite) NSColor *backgroundViewBackgroundColor; @property (nonatomic, copy, nullable, readwrite) NSColor *backgroundViewDividerColor; @property (nonatomic, assign, readwrite) CGFloat backgroundViewContentBorderPadding; @end @implementation TVCMainWindowTextViewAppearance #pragma mark - #pragma mark Initialization - (nullable instancetype)initWithWindow:(TVCMainWindow *)mainWindow { NSParameterAssert(mainWindow != nil); NSURL *appearanceLocation = [self.class appearanceLocation]; BOOL forRetinaDisplay = mainWindow.runningInHighResolutionMode; if ((self = [super initWithAppearanceAtURL:appearanceLocation forRetinaDisplay:forRetinaDisplay])) { [self prepareInitialState]; return self; } return nil; } + (NSURL *)appearanceLocation { return [RZMainBundle() URLForResource:@"TVCMainWindowTextViewAppearance" withExtension:@"plist"]; } - (void)prepareInitialState { NSDictionary *properties = self.appearanceProperties; NSDictionary *textView = properties[@"Text View"]; self.textViewInset = [self sizeInGroup:textView withKey:@"inset"]; self.textViewTextColor = [self colorInGroup:textView withKey:@"normalTextColor"]; self.textViewPlaceholderTextColor = [self colorInGroup:textView withKey:@"placeholderTextColor"]; self.textViewBackgroundColorActiveWindow = [self colorInGroup:textView withKey:@"backgroundColor" forActiveWindow:YES]; self.textViewBackgroundColorInactiveWindow = [self colorInGroup:textView withKey:@"backgroundColor" forActiveWindow:NO]; self.textViewOutlineColorActiveWindow = [self colorInGroup:textView withKey:@"outlineColor" forActiveWindow:YES]; self.textViewOutlineColorInactiveWindow = [self colorInGroup:textView withKey:@"outlineColor" forActiveWindow:NO]; self.textViewInsideShadowColorActiveWindow = [self colorInGroup:textView withKey:@"insideShadowColor" forActiveWindow:YES]; self.textViewInsideShadowColorInactiveWindow = [self colorInGroup:textView withKey:@"insideShadowColor" forActiveWindow:NO]; self.textViewInsideGradientActiveWindow = [self gradientInGroup:textView withKey:@"insideGradient" forActiveWindow:YES]; self.textViewInsideGradientInactiveWindow = [self gradientInGroup:textView withKey:@"insideGradient" forActiveWindow:NO]; self.textViewOutsidePrimaryShadowColorActiveWindow = [self colorInGroup:textView withKey:@"outsidePrimaryShadowColor" forActiveWindow:YES]; self.textViewOutsidePrimaryShadowColorInactiveWindow = [self colorInGroup:textView withKey:@"outsidePrimaryShadowColor" forActiveWindow:NO]; self.textViewOutsideSecondaryShadowColorActiveWindow = [self colorInGroup:textView withKey:@"outsideSecondaryShadowColor" forActiveWindow:YES]; self.textViewOutsideSecondaryShadowColorInactiveWindow = [self colorInGroup:textView withKey:@"outsideSecondaryShadowColor" forActiveWindow:NO]; self.textViewFont = [self fontInGroup:textView withKey:@"font"]; self.textViewFontLarge = [self fontInGroup:textView withKey:@"fontLarge"]; self.textViewFontExtraLarge = [self fontInGroup:textView withKey:@"fontExtraLarge"]; self.textViewFontHumongous = [self fontInGroup:textView withKey:@"fontHumongous"]; NSDictionary *backgroundView = properties[@"Background View"]; self.backgroundViewBackgroundColor = [self colorInGroup:backgroundView withKey:@"backgroundColor"]; self.backgroundViewDividerColor = [self colorInGroup:backgroundView withKey:@"dividerColor"]; self.backgroundViewContentBorderPadding = [self measurementInGroup:backgroundView withKey:@"contentBorderPadding"]; [self flushAppearanceProperties]; } #pragma mark - #pragma mark Everything Else - (BOOL)preferredTextViewFontChanged { return (self.textViewPreferredFontSize != [TPCPreferences mainTextViewFontSize]); } - (nullable NSFont *)textViewPreferredFont { TVCMainWindowTextViewFontSize preferredFontSize = [TPCPreferences mainTextViewFontSize]; self.textViewPreferredFontSize = preferredFontSize; NSFont *preferredFont = nil; if (preferredFontSize == TVCMainWindowTextViewFontSizeNormal) { preferredFont = self.textViewFont; } else if (preferredFontSize == TVCMainWindowTextViewFontSizeLarge) { preferredFont = self.textViewFontLarge; } else if (preferredFontSize == TVCMainWindowTextViewFontSizeExtraLarge) { preferredFont = self.textViewFontExtraLarge; } else if (preferredFontSize == TVCMainWindowTextViewFontSizeHumongous) { preferredFont = self.textViewFontHumongous; } return preferredFont; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Main Window/TVCMainWindowTitlebarAccessoryView.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSViewHelperPrivate.h" #import "TVCMainWindow.h" #import "TVCMainWindowAppearance.h" #import "TVCMainWindowTitlebarAccessoryViewPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TVCMainWindowTitlebarAccessoryView @end @implementation TVCMainWindowTitlebarAccessoryViewController @end @interface TVCMainWindowTitlebarAccessoryViewLockButton () @property (nonatomic, assign) BOOL drawsCustomBackgroundColor; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *lockButtonLeftMarginConstraint; @property (nonatomic, weak) IBOutlet NSLayoutConstraint *lockButtonRightMarginConstraint; /* Accessory views are wrapped around an NSToolbarFullScreenWindow object when in full screen. That means the view cannot use the helper property in NSView named -mainWindow which wraps around -window. Instead, we have to be explicit and use a reference. */ @property (nonatomic, weak) IBOutlet TVCMainWindow *mainWindowRef; @end @implementation TVCMainWindowTitlebarAccessoryViewLockButton - (void)sizeToFit { [super sizeToFit]; /* NSTitlebarAccessoryViewController is not very friendly when it comes to allowing us to specify an NSLayoutConstraint based width for our view and updating the width of its clip view based on changes to that. Lucky for us NSTitlebarAccessoryViewController at least monitors the frame value for its associated view which means if we manually specify the width in its frame, then we can at least force a resize then. */ CGFloat buttonWidth = NSWidth(self.frame); CGFloat buttonLeftMargin = self.lockButtonLeftMarginConstraint.constant; CGFloat buttonRightMargin = self.lockButtonRightMarginConstraint.constant; CGFloat totalViewWidth = (buttonLeftMargin + buttonWidth + buttonRightMargin); NSRect superviewFrame = self.superview.frame; superviewFrame.size.width = totalViewWidth; self.superview.frame = superviewFrame; return; } - (void)awakeFromNib { [super awakeFromNib]; [self disableDrawingCustomBackgroundColor]; } - (void)positionImageOverContent { [self.cell setImagePosition:NSImageOverlaps]; } - (void)positionImageOnLeftSide { [self.cell setImagePosition:NSImageLeft]; } - (void)setIconAsLocked { NSImage *iconImage = [NSImage imageNamed:@"NSLockLockedTemplate"]; self.image = iconImage; } - (void)setIconAsUnlocked { NSImage *iconImage = [NSImage imageNamed:@"NSLockUnlockedTemplate"]; self.image = iconImage; } - (void)disableDrawingCustomBackgroundColor { self.cell.backgroundStyle = NSBackgroundStyleRaised; self.drawsCustomBackgroundColor = NO; } - (void)enableDrawingCustomBackgroundColor { self.cell.backgroundStyle = NSBackgroundStyleLowered; self.drawsCustomBackgroundColor = YES; } - (void)drawRect:(NSRect)dirtyRect { if ([self needsToDrawRect:dirtyRect] == NO) { return; } if (self.drawsCustomBackgroundColor) { [self drawInterior]; } [super drawRect:dirtyRect]; } - (void)drawInterior { if (self.mainWindow.isActiveForDrawing == NO) { return; } TVCMainWindowAppearance *appearance = self.mainWindowRef.userInterfaceObjects; if (appearance == nil) { return; } [self drawInteriorForActiveWindowWithAppearance:appearance]; } - (void)drawInteriorForActiveWindowWithAppearance:(TVCMainWindowAppearance *)appearance { NSParameterAssert(appearance != nil); /* We get the bounds of the object and tweak it just slightly to match what it actually is. After that, we draw our color in behind it to fake the background. */ NSRect controllerFrame = self.bounds; controllerFrame.size.height -= 1.0; NSColor *controllerBackgroundColor = appearance.titlebarAccessoryViewBackgroundColorActiveWindow; NSBezierPath *drawingPath = [NSBezierPath bezierPathWithRoundedRect:controllerFrame xRadius:4.0 yRadius:4.0]; [controllerBackgroundColor set]; [drawingPath fill]; } - (void)applicationAppearanceChanged { TVCMainWindowAppearance *appearance = self.mainWindowRef.userInterfaceObjects; [self _updateAppearance:appearance]; } - (void)_updateAppearance:(TVCMainWindowAppearance *)appearance { NSParameterAssert(appearance != nil); self.lockButtonLeftMarginConstraint.constant = appearance.titlebarAccessoryViewLeftMargin; self.lockButtonRightMarginConstraint.constant = appearance.titlebarAccessoryViewRightMargin; [self sizeToFit]; self.needsDisplay = YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Preferences/TVCNotificationConfigurationViewController.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOLocalization.h" #import "TLONotificationConfigurationPrivate.h" #import "TLOSoundPlayer.h" #import "TVCNotificationConfigurationViewControllerPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _alertSoundsDefaultSoundIndex 0 #define _alertSoundsNoSoundIndex 2 @interface TVCNotificationConfigurationViewController () @property (nonatomic, weak) NSView *attachedView; @property (nonatomic, strong) IBOutlet NSView *contentView; @property (nonatomic, weak) IBOutlet NSButton *alertBounceDockIconButton; @property (nonatomic, weak) IBOutlet NSButton *alertBounceDockIconRepeatedlyButton; @property (nonatomic, weak) IBOutlet NSButton *alertDisableWhileAwayButton; @property (nonatomic, weak) IBOutlet NSButton *alertPushNotificationButton; @property (nonatomic, weak) IBOutlet NSButton *alertSpeakEventButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *alertSoundChoiceButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *alertTypeChoiceButton; @property (nonatomic, weak) IBOutlet NSTextField *alertNotificationDestinationTextField; @property (nonatomic, strong) TLONotificationConfiguration *activeAlert; @property (nonatomic, assign) BOOL activeAlertPropertyChangedByUser; @property (nonatomic, copy) NSArray *alertSounds; - (IBAction)onChangedAlertBounceDockIcon:(id)sender; - (IBAction)onChangedAlertBounceDockIconRepeatedly:(id)sender; - (IBAction)onChangedAlertDisableWhileAway:(id)sender; - (IBAction)onChangedAlertPushNotification:(id)sender; - (IBAction)onChangedAlertSound:(id)sender; - (IBAction)onChangedAlertSpoken:(id)sender; - (IBAction)onChangedAlertType:(id)sender; @end @implementation TVCNotificationConfigurationViewController - (instancetype)init { if ((self = [super init])) { [self prepareInitialState]; return self; } return nil; } - (void)dealloc { [self stopObservingActiveAlert]; } - (void)prepareInitialState { [RZMainBundle() loadNibNamed:@"TVCNotificationConfigurationView" owner:self topLevelObjects:nil]; [self updateAvailableSounds]; self.alertNotificationDestinationTextField.stringValue = TXTLS(@"TVCNotificationConfigurationView[br6-di]"); } - (void)attachToView:(NSView *)view { NSParameterAssert(view != nil); if (self.attachedView == nil) { self.attachedView = view; } else { NSAssert(NO, @"View is already attached to a view"); } NSView *contentView = self.contentView; [view addSubview:contentView]; [view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[contentView]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(contentView)]]; [view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[contentView]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(contentView)]]; } - (void)setAllowsMixedState:(BOOL)allowsMixedState { if (self->_allowsMixedState != allowsMixedState) { self->_allowsMixedState = allowsMixedState; [self updateMixedState]; } } - (void)updateMixedState { BOOL allowsMixedState = self.allowsMixedState; self.alertSpeakEventButton.allowsMixedState = allowsMixedState; self.alertBounceDockIconButton.allowsMixedState = allowsMixedState; self.alertBounceDockIconRepeatedlyButton.allowsMixedState = allowsMixedState; self.alertDisableWhileAwayButton.allowsMixedState = allowsMixedState; self.alertPushNotificationButton.allowsMixedState = allowsMixedState; } - (void)setNotifications:(NSArray *)notifications { if (self->_notifications != notifications) { self->_notifications = [notifications copy]; [self availableAlertsChanged]; } } - (void)availableAlertsChanged { if (self.notifications.count == 0) { [self resetControls]; } else { [self updateAlertSelection]; } } - (void)resetControls { [self.alertTypeChoiceButton removeAllItems]; self.alertSpeakEventButton.state = NSControlStateValueOff; self.alertBounceDockIconButton.state = NSControlStateValueOff; self.alertBounceDockIconRepeatedlyButton.enabled = NO; self.alertBounceDockIconRepeatedlyButton.state = NSControlStateValueOff; self.alertDisableWhileAwayButton.state = NSControlStateValueOff; self.alertPushNotificationButton.state = NSControlStateValueOff; [self.alertSoundChoiceButton removeAllItems]; } - (void)updateAlertSelection { [self.alertTypeChoiceButton removeAllItems]; [self.notifications enumerateObjectsUsingBlock:^(id alert, NSUInteger index, BOOL *stop) { if ([alert isKindOfClass:[TLONotificationConfiguration class]]) { NSMenuItem *item = [NSMenuItem new]; item.tag = index; item.title = [alert displayName]; [self.alertTypeChoiceButton.menu addItem:item]; } else { [self.alertTypeChoiceButton.menu addItem:[NSMenuItem separatorItem]]; } }]; [self.alertTypeChoiceButton selectItemAtIndex:0]; [self onChangedAlertType:nil]; } - (void)reload { TLONotificationConfiguration *alert = self.activeAlert; self.alertSpeakEventButton.state = alert.speakEvent; self.alertBounceDockIconButton.state = alert.bounceDockIcon; self.alertBounceDockIconRepeatedlyButton.enabled = (self.alertBounceDockIconButton.state != NSControlStateValueOff); self.alertBounceDockIconRepeatedlyButton.state = alert.bounceDockIconRepeatedly; self.alertDisableWhileAwayButton.state = alert.disabledWhileAway; self.alertPushNotificationButton.state = alert.pushNotification; NSString *alertSound = alert.alertSound; if (alertSound == nil) { [self.alertSoundChoiceButton selectItemAtIndex:_alertSoundsDefaultSoundIndex]; } else if ([alertSound isEqualToString:TXNoAlertSoundPreferenceValue]) { [self.alertSoundChoiceButton selectItemAtIndex:_alertSoundsNoSoundIndex]; } else { NSUInteger soundIndex = [self.alertSounds indexOfObject:alert.alertSound]; if (soundIndex == NSNotFound) { [self.alertSoundChoiceButton selectItemAtIndex:_alertSoundsNoSoundIndex]; } else { [self.alertSoundChoiceButton selectItemAtIndex:soundIndex]; } } } - (void)onChangedAlertType:(id)sender { TXNotificationType alertTag = (TXNotificationType)self.alertTypeChoiceButton.selectedTag; self.activeAlert = self.notifications[alertTag]; [self reload]; } - (void)onChangedAlertPushNotification:(id)sender { self.activeAlertPropertyChangedByUser = YES; TLONotificationConfiguration *alert = self.activeAlert; alert.pushNotification = self.alertPushNotificationButton.state; } - (void)onChangedAlertSpoken:(id)sender { self.activeAlertPropertyChangedByUser = YES; TLONotificationConfiguration *alert = self.activeAlert; alert.speakEvent = self.alertSpeakEventButton.state; } - (void)onChangedAlertDisableWhileAway:(id)sender { self.activeAlertPropertyChangedByUser = YES; TLONotificationConfiguration *alert = self.activeAlert; alert.disabledWhileAway = self.alertDisableWhileAwayButton.state; } - (void)onChangedAlertBounceDockIcon:(id)sender { self.activeAlertPropertyChangedByUser = YES; TLONotificationConfiguration *alert = self.activeAlert; alert.bounceDockIcon = self.alertBounceDockIconButton.state; self.alertBounceDockIconRepeatedlyButton.enabled = (self.alertBounceDockIconButton.state == NSControlStateValueOn); } - (void)onChangedAlertBounceDockIconRepeatedly:(id)sender { self.activeAlertPropertyChangedByUser = YES; TLONotificationConfiguration *alert = self.activeAlert; alert.bounceDockIconRepeatedly = self.alertBounceDockIconRepeatedlyButton.state; } - (void)onChangedAlertSound:(id)sender { self.activeAlertPropertyChangedByUser = YES; TLONotificationConfiguration *alert = self.activeAlert; NSString *alertSound = self.alertSoundChoiceButton.titleOfSelectedItem; if ([alertSound isEqualToString:[TLONotificationConfiguration localizedAlertDefaultSoundTitle]]) { alertSound = nil; } else if ([alertSound isEqualToString:[TLONotificationConfiguration localizedAlertNoSoundTitle]]) { alertSound = TXNoAlertSoundPreferenceValue; } if (alertSound) { [TLOSoundPlayer playAlertSound:alertSound]; } alert.alertSound = alertSound; } - (void)setActiveAlert:(TLONotificationConfiguration *)activeAlert { NSParameterAssert(activeAlert != nil); if (self->_activeAlert != activeAlert) { [self stopObservingActiveAlert]; self->_activeAlert = activeAlert; [self startObservingActiveAlert]; } } - (void)startObservingActiveAlert { TLONotificationConfiguration *activeAlert = self.activeAlert; if (activeAlert == nil) { return; } [activeAlert addObserver:self forKeyPath:@"alertSound" options:NSKeyValueObservingOptionNew context:NULL]; [activeAlert addObserver:self forKeyPath:@"speakEvent" options:NSKeyValueObservingOptionNew context:NULL]; [activeAlert addObserver:self forKeyPath:@"pushNotification" options:NSKeyValueObservingOptionNew context:NULL]; [activeAlert addObserver:self forKeyPath:@"disableWhileAway" options:NSKeyValueObservingOptionNew context:NULL]; [activeAlert addObserver:self forKeyPath:@"bounceDockIcon" options:NSKeyValueObservingOptionNew context:NULL]; [activeAlert addObserver:self forKeyPath:@"bounceDockIconRepeatedly" options:NSKeyValueObservingOptionNew context:NULL]; } - (void)stopObservingActiveAlert { TLONotificationConfiguration *activeAlert = self.activeAlert; if (activeAlert == nil) { return; } [activeAlert removeObserver:self forKeyPath:@"alertSound"]; [activeAlert removeObserver:self forKeyPath:@"speakEvent"]; [activeAlert removeObserver:self forKeyPath:@"pushNotification"]; [activeAlert removeObserver:self forKeyPath:@"disableWhileAway"]; [activeAlert removeObserver:self forKeyPath:@"bounceDockIcon"]; [activeAlert removeObserver:self forKeyPath:@"bounceDockIconRepeatedly"]; } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if (object == self.activeAlert) { if (self.activeAlertPropertyChangedByUser) { self.activeAlertPropertyChangedByUser = NO; return; } LogToConsoleDebug("Reloading user interface because key %{public}@ changed remotely", keyPath); [self reload]; } } - (void)updateAvailableSounds { [self.alertSoundChoiceButton removeAllItems]; self.alertSounds = [self availableSounds]; [self.alertSounds enumerateObjectsUsingBlock:^(id alertSound, NSUInteger index, BOOL *stop) { if ([alertSound isKindOfClass:[NSString class]]) { NSMenuItem *item = [NSMenuItem new]; item.title = alertSound; [self.alertSoundChoiceButton.menu addItem:item]; } else { [self.alertSoundChoiceButton.menu addItem:alertSound]; } }]; [self.alertSoundChoiceButton selectItemAtIndex:0]; } - (NSArray *)availableSounds { NSMutableArray *sounds = [NSMutableArray array]; [sounds addObject:[TLONotificationConfiguration localizedAlertDefaultSoundTitle]]; [sounds addObject:[NSMenuItem separatorItem]]; [sounds addObject:[TLONotificationConfiguration localizedAlertNoSoundTitle]]; [sounds addObject:[NSMenuItem separatorItem]]; [sounds addObjectsFromArray:[TLOSoundPlayer uniqueListOfSounds]]; return [sounds copy]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Server List/TVCServerList.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSViewHelperPrivate.h" #import "TXMasterController.h" #import "TXMenuControllerPrivate.h" #import "TVCMainWindowPrivate.h" #import "TPCPreferencesLocal.h" #import "TVCServerListAppearancePrivate.h" #import "TVCServerListCellPrivate.h" #import "TVCServerListPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TVCServerListDragType = @"TVCServerListDragType"; @interface TVCServerList () @property (nonatomic, strong, readwrite) TVCServerListAppearance *userInterfaceObjects; @property (nonatomic, weak, readwrite) IBOutlet NSVisualEffectView *visualEffectView; @property (nonatomic, assign, readwrite) BOOL leftMouseIsDownInView; @end @implementation TVCServerList - (void)viewDidMoveToWindow { [super viewDidMoveToWindow]; TVCMainWindow *mainWindow = self.mainWindow; if (mainWindow == nil) { [RZNotificationCenter() removeObserver:self]; return; } [RZNotificationCenter() addObserver:self selector:@selector(windowDidBecomeKey:) name:NSWindowDidBecomeKeyNotification object:mainWindow]; [RZNotificationCenter() addObserver:self selector:@selector(windowDidResignKey:) name:NSWindowDidResignKeyNotification object:mainWindow]; [RZNotificationCenter() addObserver:self selector:@selector(mainWindowRequiresRedraw:) name:TVCMainWindowRedrawSubviewsNotification object:mainWindow]; } #pragma mark - #pragma mark Additions/Removal - (void)addItemToList:(NSUInteger)rowIndex inParent:(nullable id)parent { [self insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:rowIndex] inParent:parent withAnimation:(NSTableViewAnimationEffectFade | NSTableViewAnimationSlideRight)]; if (parent) { [self reloadItem:parent]; } } - (void)removeItemFromList:(id)object { NSParameterAssert(object != nil); NSInteger rowIndex = [self rowForItem:object]; NSAssert((rowIndex >= 0), @"Object does not exist on outline view"); id parentItem = [self parentForItem:object]; if (parentItem) { NSArray *childrenItems = [self itemsFromParentGroup:parentItem]; rowIndex = [childrenItems indexOfObject:object]; } else { NSArray *groupItems = self.groupItems; rowIndex = [groupItems indexOfObject:object]; } NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:rowIndex]; [self removeItemsAtIndexes:indexSet inParent:parentItem withAnimation:(NSTableViewAnimationEffectFade | NSTableViewAnimationSlideLeft)]; if (parentItem) { [self reloadItem:parentItem]; } } - (void)moveItemAtIndex:(NSInteger)fromIndex inParent:(nullable id)oldParent toIndex:(NSInteger)toIndex inParent:(nullable id)newParent { if (fromIndex < toIndex) { [super moveItemAtIndex:fromIndex inParent:oldParent toIndex:(toIndex - 1) inParent:newParent]; } else { [super moveItemAtIndex:fromIndex inParent:oldParent toIndex:toIndex inParent:newParent]; } } #pragma mark - #pragma mark Drawing Updates - (void)refreshAllDrawings { [self refreshAllDrawings:NO]; } - (void)refreshAllDrawings:(BOOL)skipOcclusionCheck { for (NSUInteger i = 0; i < self.numberOfRows; i++) { [self refreshDrawingForRow:i skipOcclusionCheck:skipOcclusionCheck]; } } - (void)refreshDrawingForRows:(NSIndexSet *)rowIndexes { [self refreshDrawingForRows:rowIndexes skipOcclusionCheck:NO]; } - (void)refreshDrawingForRows:(NSIndexSet *)rowIndexes skipOcclusionCheck:(BOOL)skipOcclusionCheck { NSParameterAssert(rowIndexes != nil); [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { [self refreshDrawingForRow:index skipOcclusionCheck:skipOcclusionCheck]; }]; } - (void)refreshDrawingForRow:(NSInteger)rowIndex { [self refreshDrawingForRow:rowIndex skipOcclusionCheck:NO]; } - (void)refreshDrawingForRow:(NSInteger)rowIndex skipOcclusionCheck:(BOOL)skipOcclusionCheck { if (rowIndex < 0) { return; } if (skipOcclusionCheck == NO && self.mainWindow.occluded) { return; } __kindof TVCServerListCell *rowView = [self viewAtColumn:0 row:rowIndex makeIfNecessary:NO]; rowView.needsDisplay = YES; } - (void)refreshDrawingForItem:(IRCTreeItem *)cellItem { [self refreshDrawingForItem:cellItem skipOcclusionCheck:NO]; } - (void)refreshDrawingForItem:(IRCTreeItem *)cellItem skipOcclusionCheck:(BOOL)skipOcclusionCheck { NSParameterAssert(cellItem != nil); NSInteger rowIndex = [self rowForItem:cellItem]; [self refreshDrawingForRow:rowIndex skipOcclusionCheck:skipOcclusionCheck]; } - (void)refreshMessageCountForItem:(IRCTreeItem *)cellItem { [self refreshMessageCountForItem:cellItem skipOcclusionCheck:NO]; } - (void)refreshMessageCountForItem:(IRCTreeItem *)cellItem skipOcclusionCheck:(BOOL)skipOcclusionCheck { NSParameterAssert(cellItem != nil); NSInteger rowIndex = [self rowForItem:cellItem]; [self refreshMessageCountForRow:rowIndex skipOcclusionCheck:skipOcclusionCheck]; } - (void)refreshAllUnreadMessageCountBadges { [self refreshAllUnreadMessageCountBadges:NO]; } - (void)refreshAllUnreadMessageCountBadges:(BOOL)skipOcclusionCheck { for (NSUInteger i = 0; i < self.numberOfRows; i++) { [self refreshMessageCountForRow:i skipOcclusionCheck:skipOcclusionCheck]; } } - (void)refreshMessageCountForRows:(NSIndexSet *)rowIndexes { [self refreshMessageCountForRows:rowIndexes skipOcclusionCheck:NO]; } - (void)refreshMessageCountForRows:(NSIndexSet *)rowIndexes skipOcclusionCheck:(BOOL)skipOcclusionCheck { NSParameterAssert(rowIndexes != nil); [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { [self refreshMessageCountForRow:index skipOcclusionCheck:skipOcclusionCheck]; }]; } - (void)refreshMessageCountForRow:(NSInteger)rowIndex { [self refreshMessageCountForRow:rowIndex skipOcclusionCheck:NO]; } - (void)refreshMessageCountForRow:(NSInteger)rowIndex skipOcclusionCheck:(BOOL)skipOcclusionCheck { if (rowIndex < 0) { return; } if (skipOcclusionCheck == NO && self.mainWindow.occluded) { return; } __kindof TVCServerListCell *rowView = [self viewAtColumn:0 row:rowIndex makeIfNecessary:NO]; BOOL isChildItem = [rowView isKindOfClass:[TVCServerListCellChildItem class]]; if (isChildItem) { [rowView populateMessageCountBadge]; } } - (BOOL)allowsVibrancy { return YES; } - (void)updateVibrancyWithAppearance:(TVCServerListAppearance *)appearance { NSParameterAssert(appearance != nil); NSVisualEffectView *visualEffectView = self.visualEffectView; if ([TPCPreferences disableSidebarTranslucency]) { visualEffectView.state = NSVisualEffectStateInactive; } else { visualEffectView.state = NSVisualEffectStateFollowsWindowActiveState; } visualEffectView.material = NSVisualEffectMaterialSidebar; } - (void)applicationAppearanceChanged { TVCServerListAppearance *appearance = self.mainWindow.userInterfaceObjects.serverList; [self _updateAppearance:appearance]; [self invalidateBackgroundForSelection]; [self refreshAllDrawings:YES]; } - (void)systemAppearanceChanged { // [self invalidateBackgroundForSelection]; } - (void)_updateAppearance:(TVCServerListAppearance *)appearance { NSParameterAssert(appearance != nil); /* We assign a strong reference to these instead of returning the original value every time so that there are no race conditions for when it changes. */ self.userInterfaceObjects = appearance; [self updateVibrancyWithAppearance:appearance]; if (appearance.isDarkAppearance) { self.enclosingScrollView.scrollerKnobStyle = NSScrollerKnobStyleLight; } else { self.enclosingScrollView.scrollerKnobStyle = NSScrollerKnobStyleDark; } self.needsDisplay = YES; } - (void)windowDidBecomeKey:(NSNotification *)notification { [self windowKeyStateChanged:notification]; } - (void)windowDidResignKey:(NSNotification *)notification { [self windowKeyStateChanged:notification]; } - (void)windowKeyStateChanged:(NSNotification *)notification { [self respondToRequiresRedraw]; } - (void)mainWindowRequiresRedraw:(NSNotification *)notification { [self respondToRequiresRedraw]; } - (void)respondToRequiresRedraw { [self refreshAllDrawings:YES]; } #pragma mark - #pragma mark Events - (nullable NSMenu *)menuForEvent:(NSEvent *)theEvent { NSInteger rowBeneathMouse = self.rowBeneathMouse; if (rowBeneathMouse >= 0) { if (rowBeneathMouse != self.selectedRow || self.numberOfSelectedRows > 1) { [self selectItemAtIndex:rowBeneathMouse]; } } else { return menuController().serverListNoSelectionMenu; } return self.menu; } - (void)mouseDown:(NSEvent *)theEvent { self.leftMouseIsDownInView = YES; [super mouseDown:theEvent]; } - (void)mouseUp:(NSEvent *)theEvent { self.leftMouseIsDownInView = NO; [super mouseUp:theEvent]; } - (void)keyDown:(NSEvent *)e { if (self.keyDelegate == nil) { return; } switch (e.keyCode) { case 125: // down arrow case 126: // up arrow case 123: // left arrow case 124: // right arrow case 116: // page up case 121: // page down { break; } default: { [self.keyDelegate serverListKeyDown:e]; break; } } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Server List/TVCServerListAppearance.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSColorHelper.h" #import "NSObjectHelperPrivate.h" #import "NSViewHelperPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TVCAppearancePrivate.h" #import "TVCMainWindow.h" #import "TVCServerListPrivate.h" #import "TVCServerListAppearancePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCServerListAppearance () @property (nonatomic, weak, readwrite) TVCServerList *serverList; @property (nonatomic, assign, readwrite) CGFloat defaultWidth; @property (nonatomic, assign, readwrite) CGFloat minimumWidth; @property (nonatomic, assign, readwrite) CGFloat maximumWidth; @property (nonatomic, copy, nullable, readwrite) NSColor *rowSelectionColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *rowSelectionColorInactiveWindow; #pragma mark - #pragma mark Server Cell @property (nonatomic, assign, readwrite) BOOL serverRowEmphasized; @property (nonatomic, copy, nullable, readwrite) NSColor *serverTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *serverTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *serverDisabledTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *serverDisabledTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *serverSelectedTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *serverSelectedTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSFont *serverFont; @property (nonatomic, copy, nullable, readwrite) NSFont *serverFontSelected; #pragma mark - #pragma mark Channel Cell @property (nonatomic, assign, readwrite) BOOL channelRowEmphasized; @property (nonatomic, assign, readwrite) CGFloat serverLabelLeftMargin; @property (nonatomic, copy, nullable, readwrite) NSColor *channelTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelDisabledTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelDisabledTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelSelectedTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelSelectedTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelErroneousTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelErroneousTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelHighlightTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *channelHighlightTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSFont *channelFont; @property (nonatomic, copy, nullable, readwrite) NSFont *channelFontSelected; #pragma mark - #pragma mark Message Count Badge @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeBackgroundColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeBackgroundColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeSelectedBackgroundColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeSelectedBackgroundColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeHighlightBackgroundColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeHighlightBackgroundColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeSelectedTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeSelectedTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeHighlightTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *unreadBadgeHighlightTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSFont *unreadBadgeFont; @property (nonatomic, copy, nullable, readwrite) NSFont *unreadBadgeFontSelected; @property (nonatomic, assign, readwrite) CGFloat unreadBadgeMinimumWidth; @property (nonatomic, assign, readwrite) CGFloat unreadBadgeHeight; @property (nonatomic, assign, readwrite) CGFloat unreadBadgePadding; @end @implementation TVCServerListAppearance #pragma mark - #pragma mark Initialization - (nullable instancetype)initWithServerList:(TVCServerList *)serverList inWindow:(TVCMainWindow *)mainWindow { NSParameterAssert(serverList != nil); NSParameterAssert(mainWindow != nil); NSURL *appearanceLocation = [self.class appearanceLocation]; /* Don't access -mainWindow in serverList to get this value because it may not be on a window at the time this is called. */ BOOL forRetinaDisplay = mainWindow.runningInHighResolutionMode; if ((self = [super initWithAppearanceAtURL:appearanceLocation forRetinaDisplay:forRetinaDisplay])) { self.serverList = serverList; [self prepareInitialState]; return self; } return nil; } + (NSURL *)appearanceLocation { return [RZMainBundle() URLForResource:@"TVCServerListAppearance" withExtension:@"plist"]; } - (void)prepareInitialState { NSDictionary *properties = self.appearanceProperties; self.defaultWidth = [self measurementForKey:@"defaultWidth"]; self.minimumWidth = [self measurementForKey:@"minimumWidth"]; self.maximumWidth = [self measurementForKey:@"maximumWidth"]; self.rowSelectionColorActiveWindow = [self colorForKey:@"selectionColor" forActiveWindow:YES]; self.rowSelectionColorInactiveWindow = [self colorForKey:@"selectionColor" forActiveWindow:NO]; NSDictionary *serverCell = properties[@"Server Cell"]; self.serverRowEmphasized = [serverCell boolForKey:@"rowEmphasized"]; self.serverLabelLeftMargin = [serverCell floatForKey:@"labelLeftMargin"]; self.serverTextColorActiveWindow = [self colorInGroup:serverCell withKey:@"normalTextColor" forActiveWindow:YES]; self.serverTextColorInactiveWindow = [self colorInGroup:serverCell withKey:@"normalTextColor" forActiveWindow:NO]; self.serverDisabledTextColorActiveWindow = [self colorInGroup:serverCell withKey:@"disabledTextColor" forActiveWindow:YES]; self.serverDisabledTextColorInactiveWindow = [self colorInGroup:serverCell withKey:@"disabledTextColor" forActiveWindow:NO]; self.serverSelectedTextColorActiveWindow = [self colorInGroup:serverCell withKey:@"selectedTextColor" forActiveWindow:YES]; self.serverSelectedTextColorInactiveWindow = [self colorInGroup:serverCell withKey:@"selectedTextColor" forActiveWindow:NO]; self.serverFont = [self fontInGroup:serverCell withKey:@"font"]; self.serverFontSelected = [self fontInGroup:serverCell withKey:@"fontSelected"]; NSDictionary *channelCell = properties[@"Channel Cell"]; self.channelRowEmphasized = [channelCell boolForKey:@"rowEmphasized"]; self.channelTextColorActiveWindow = [self colorInGroup:channelCell withKey:@"normalTextColor" forActiveWindow:YES]; self.channelTextColorInactiveWindow = [self colorInGroup:channelCell withKey:@"normalTextColor" forActiveWindow:NO]; self.channelDisabledTextColorActiveWindow = [self colorInGroup:channelCell withKey:@"disabledTextColor" forActiveWindow:YES]; self.channelDisabledTextColorInactiveWindow = [self colorInGroup:channelCell withKey:@"disabledTextColor" forActiveWindow:NO]; self.channelSelectedTextColorActiveWindow = [self colorInGroup:channelCell withKey:@"selectedTextColor" forActiveWindow:YES]; self.channelSelectedTextColorInactiveWindow = [self colorInGroup:channelCell withKey:@"selectedTextColor" forActiveWindow:NO]; self.channelErroneousTextColorActiveWindow = [self colorInGroup:channelCell withKey:@"erroneousTextColor" forActiveWindow:YES]; self.channelErroneousTextColorInactiveWindow = [self colorInGroup:channelCell withKey:@"erroneousTextColor" forActiveWindow:NO]; self.channelHighlightTextColorActiveWindow = [self colorInGroup:channelCell withKey:@"highlightTextColor" forActiveWindow:YES]; self.channelHighlightTextColorInactiveWindow = [self colorInGroup:channelCell withKey:@"highlightTextColor" forActiveWindow:NO]; self.channelFont = [self fontInGroup:channelCell withKey:@"font"]; self.channelFontSelected = [self fontInGroup:channelCell withKey:@"fontSelected"]; NSDictionary *unreadBadge = properties[@"Unread Badge"]; self.unreadBadgeBackgroundColorActiveWindow = [self colorInGroup:unreadBadge withKey:@"normalBackgroundColor" forActiveWindow:YES]; self.unreadBadgeBackgroundColorInactiveWindow = [self colorInGroup:unreadBadge withKey:@"normalBackgroundColor" forActiveWindow:NO]; self.unreadBadgeSelectedBackgroundColorActiveWindow = [self colorInGroup:unreadBadge withKey:@"selectedBackgroundColor" forActiveWindow:YES]; self.unreadBadgeSelectedBackgroundColorInactiveWindow = [self colorInGroup:unreadBadge withKey:@"selectedBackgroundColor" forActiveWindow:NO]; self.unreadBadgeHighlightBackgroundColorActiveWindow = [self colorInGroup:unreadBadge withKey:@"highlightBackgroundColor" forActiveWindow:YES]; self.unreadBadgeHighlightBackgroundColorInactiveWindow = [self colorInGroup:unreadBadge withKey:@"highlightBackgroundColor" forActiveWindow:NO]; self.unreadBadgeTextColorActiveWindow = [self colorInGroup:unreadBadge withKey:@"normalTextColor" forActiveWindow:YES]; self.unreadBadgeTextColorInactiveWindow = [self colorInGroup:unreadBadge withKey:@"normalTextColor" forActiveWindow:NO]; self.unreadBadgeSelectedTextColorActiveWindow = [self colorInGroup:unreadBadge withKey:@"selectedTextColor" forActiveWindow:YES]; self.unreadBadgeSelectedTextColorInactiveWindow = [self colorInGroup:unreadBadge withKey:@"selectedTextColor" forActiveWindow:NO]; self.unreadBadgeHighlightTextColorActiveWindow = [self colorInGroup:unreadBadge withKey:@"highlightTextColor" forActiveWindow:YES]; self.unreadBadgeHighlightTextColorInactiveWindow = [self colorInGroup:unreadBadge withKey:@"highlightTextColor" forActiveWindow:NO]; self.unreadBadgeFont = [self fontInGroup:unreadBadge withKey:@"font"]; self.unreadBadgeFontSelected = [self fontInGroup:unreadBadge withKey:@"fontSelected"]; self.unreadBadgeMinimumWidth = [self measurementInGroup:unreadBadge withKey:@"minimumWidth"]; self.unreadBadgeHeight = [self measurementInGroup:unreadBadge withKey:@"height"]; self.unreadBadgePadding = [self measurementInGroup:unreadBadge withKey:@"padding"]; [self flushAppearanceProperties]; } #pragma mark - #pragma mark Everything Else - (nullable NSString *)statusIconForActiveChannel:(BOOL)isActive selected:(BOOL)isSelected activeWindow:(BOOL)isActiveWindow treatAsTemplate:(BOOL *)treatAsTemplate { NSParameterAssert(treatAsTemplate != NULL); TXAppearanceType appearanceType = self.appearanceType; switch (appearanceType) { case TXAppearanceTypeBigSurLight: { /* When the window is not in focus, when this item is selected, and when we are not using vibrant dark mode; the outline view does not turn our icon to a light variant like it would do if the window was in focus and used as a template. To workaround this oddity that Apple does, we fudge the icon by using another variant of it. */ if (isActiveWindow == NO && isSelected) { *treatAsTemplate = NO; if (isActive) { return @"channelRoomStatusIconDarkActive"; } else { return @"channelRoomStatusIconDarkInactive"; } } // quirk fix *treatAsTemplate = YES; if (isActive) { return @"channelRoomStatusIconLightActive"; } else { return @"channelRoomStatusIconLightInactive"; } } // Big Sur case TXAppearanceTypeBigSurDark: { *treatAsTemplate = NO; if (isActive) { return @"channelRoomStatusIconDarkActive"; } else { return @"channelRoomStatusIconDarkInactive"; } } // Big Sur } // switch() } - (nullable NSString *)statusIconForActiveQuery:(BOOL)isActive selected:(BOOL)isSelected activeWindow:(BOOL)isActiveWindow treatAsTemplate:(BOOL *)treatAsTemplate { NSParameterAssert(treatAsTemplate != NULL); TXAppearanceType appearanceType = self.appearanceType; switch (appearanceType) { case TXAppearanceTypeBigSurLight: { *treatAsTemplate = YES; if (isActive) { return @"VibrantLightServerListViewPrivateMessageUserIconActive"; } else { return @"VibrantLightServerListViewPrivateMessageUserIconInactive"; } } // Big Sur case TXAppearanceTypeBigSurDark: { *treatAsTemplate = NO; if (isActive) { return @"VibrantDarkServerListViewPrivateMessageUserIconActive"; } else { return @"VibrantDarkServerListViewPrivateMessageUserIconInactive"; } } // Big Sur } // switch() } - (nullable NSColor *)unreadBadgeHighlightBackgroundColorByUser { return [RZUserDefaults() colorForKey:@"Server List Unread Message Count Badge Colors -> Highlight"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/Server List/TVCServerListCell.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSViewHelperPrivate.h" #import "TXGlobalModels.h" #import "TLOLocalization.h" #import "TPCPreferencesLocal.h" #import "IRCClient.h" #import "IRCChannel.h" #import "TVCMainWindow.h" #import "TVCServerListAppearancePrivate.h" #import "TVCServerListPrivate.h" #import "TVCServerListCellPrivate.h" NS_ASSUME_NONNULL_BEGIN @class TVCServerListCellDrawingContext; @interface TVCServerListRowCell () @property (nonatomic, weak) TVCServerList *serverList; @property (nonatomic, weak) __kindof TVCServerListCell *childCell; @property (readonly) TVCServerListAppearance *userInterfaceObjects; @property (readonly) BOOL isGroupItem; @end @interface TVCServerListCell () @property (nonatomic, weak) IBOutlet NSTextField *cellTextField; @property (nonatomic, weak) IBOutlet NSImageView *messageCountBadgeImageView; // Deactivating the constraints will dereference them. // We need to maintain a strong reference. @property (nonatomic, strong) IBOutlet NSLayoutConstraint *cellTextFieldLeftMarginConstraint; @property (nonatomic, strong) IBOutlet NSLayoutConstraint *messageCountBadgeLeadingConstraint; @property (nonatomic, strong) IBOutlet NSLayoutConstraint *messageCountBadgeTrailingConstraint; @property (readonly) BOOL isGroupItem; @property (readonly) TVCServerList *serverList; @property (readonly) __kindof TVCServerListRowCell *rowCell; @property (readonly) TVCServerListAppearance *userInterfaceObjects; @property (readonly) TVCServerListCellDrawingContext *drawingContext; @property (readonly) IRCTreeItem *cellItem; @end @interface TVCServerListCellGroupItem () @property (nonatomic, weak, nullable) NSButton *disclosureTriangle; @end @interface TVCServerListCellDrawingContext : NSObject @property (nonatomic, assign) BOOL isActive; @property (nonatomic, assign) BOOL isGroupItem; @property (nonatomic, assign) BOOL isInverted; @property (nonatomic, assign) BOOL isSelected; @property (nonatomic, assign) BOOL isSelectedFrontmost; @property (nonatomic, assign) BOOL isWindowActive; @end @implementation TVCServerListCell #pragma mark - #pragma mark Cell Drawing - (void)defineConstraints { } - (BOOL)wantsUpdateLayer { return YES; } - (NSViewLayerContentsRedrawPolicy)layerContentsRedrawPolicy { return NSViewLayerContentsRedrawOnSetNeedsDisplay; } - (void)updateLayer { [self updateDrawing]; } - (void)updateDrawing { TVCServerListCellDrawingContext *drawingContext = self.drawingContext; [self updateTextFieldInContext:drawingContext]; TVCServerListAppearance *appearance = self.userInterfaceObjects; [self updateDrawingForWithAppearance:appearance inContext:drawingContext]; } - (void)updateTextFieldInContext:(TVCServerListCellDrawingContext *)drawingContext { NSParameterAssert(drawingContext != nil); /* Update string value */ IRCTreeItem *cellItem = self.cellItem; NSString *stringValueNew = cellItem.label; NSTextField *textField = self.cellTextField; NSString *stringValueOld = textField.stringValue; if ([stringValueOld isEqualTo:stringValueNew]) { return; } textField.stringValue = stringValueNew; /* Update accessibility */ BOOL isActive = drawingContext.isActive; BOOL isGroupItem = drawingContext.isGroupItem; NSTextFieldCell *textFieldCell = textField.cell; if (isGroupItem) { if (isActive) { [textFieldCell setAccessibilityValueDescription:TXTLS(@"Accessibility[bmy-d2]", stringValueNew)]; } else { [textFieldCell setAccessibilityValueDescription:TXTLS(@"Accessibility[tu4-8u]", stringValueNew)]; } // isActive } else { if (((IRCChannel *)cellItem).isChannel == NO) { [textFieldCell setAccessibilityValueDescription:TXTLS(@"Accessibility[9sn-xp]", stringValueNew)]; } else { if (isActive) { [textFieldCell setAccessibilityValueDescription:TXTLS(@"Accessibility[75f-og]", stringValueNew)]; } else { [textFieldCell setAccessibilityValueDescription:TXTLS(@"Accessibility[edc-7o]", stringValueNew)]; } // isActive } // isChannel [self.imageView.cell setAccessibilityLabel:nil]; } // isGroupItem } - (void)updateDrawingForWithAppearance:(TVCServerListAppearance *)appearance inContext:(TVCServerListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isActive = drawingContext.isActive; BOOL isGroupItem = drawingContext.isGroupItem; BOOL isSelected = drawingContext.isSelected; BOOL isWindowActive = drawingContext.isWindowActive; if (isGroupItem == NO) { IRCTreeItem *cellItem = self.cellItem; IRCChannel *channel = (IRCChannel *)cellItem; NSString *iconName = nil; BOOL iconIsTemplate = NO; if (channel.isChannel) { iconName = [appearance statusIconForActiveChannel:isActive selected:isSelected activeWindow:isWindowActive treatAsTemplate:&iconIsTemplate]; } else { iconName = [appearance statusIconForActiveQuery:isActive selected:isSelected activeWindow:isWindowActive treatAsTemplate:&iconIsTemplate]; } // isChannel NSImage *icon = [NSImage imageNamed:iconName]; icon.template = iconIsTemplate; self.imageView.image = icon; } NSAttributedString *newValue = [self attributedTextFieldValueWithAppearance:appearance inContext:drawingContext]; self.cellTextField.attributedStringValue = newValue; if (isGroupItem == NO) { [self populateMessageCountBadgeWithAppearance:appearance inContext:drawingContext]; } } - (NSAttributedString *)attributedTextFieldValueWithAppearance:(TVCServerListAppearance *)appearance inContext:(TVCServerListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isActive = drawingContext.isActive; BOOL isGroupItem = drawingContext.isGroupItem; BOOL isSelected = drawingContext.isSelected; BOOL isWindowActive = drawingContext.isWindowActive; IRCTreeItem *cellItem = self.cellItem; BOOL isErroneous = NO; BOOL isHighlight = NO; if (isGroupItem == NO) { IRCChannel *associatedChannel = (id)cellItem; isErroneous = associatedChannel.errorOnLastJoinAttempt; isHighlight = (associatedChannel.nicknameHighlightCount > 0); } NSTextField *textField = self.cellTextField; NSAttributedString *stringValue = textField.attributedStringValue; NSMutableAttributedString *mutableStringValue = [stringValue mutableCopy]; [mutableStringValue beginEditing]; NSFont *controlFont = nil; NSColor *controlColor = nil; if (isGroupItem) { if (isSelected) { controlFont = appearance.serverFontSelected; } else { controlFont = appearance.serverFont; } // isSelected if (isSelected) { if (isWindowActive) { controlColor = appearance.serverSelectedTextColorActiveWindow; } else { controlColor = appearance.serverSelectedTextColorInactiveWindow; } // isWindowActive } else if (isActive) { if (isWindowActive) { controlColor = appearance.serverTextColorActiveWindow; } else { controlColor = appearance.serverTextColorInactiveWindow; } // isWindowActive } else { if (isWindowActive) { controlColor = appearance.serverDisabledTextColorActiveWindow; } else { controlColor = appearance.serverDisabledTextColorInactiveWindow; } // isWindowActive } } else // isGroupItem { if (isSelected) { controlFont = appearance.channelFontSelected; } else { controlFont = appearance.channelFont; } // isSelected if (isSelected) { if (isWindowActive) { controlColor = appearance.channelSelectedTextColorActiveWindow; } else { controlColor = appearance.channelSelectedTextColorInactiveWindow; } // isWindowActive } else if (isActive && isHighlight) { NSColor *customColor = appearance.unreadBadgeHighlightBackgroundColorByUser; if (customColor && [customColor isEqual:[NSColor clearColor]] == NO) { controlColor = customColor; } else { if (isWindowActive) { controlColor = appearance.channelHighlightTextColorActiveWindow; } else { controlColor = appearance.channelHighlightTextColorInactiveWindow; } // isWindowActive } // custom color set } else if (isActive) { if (isWindowActive) { controlColor = appearance.channelTextColorActiveWindow; } else { controlColor = appearance.channelTextColorInactiveWindow; } // isWindowActive } else if (isErroneous) { if (isWindowActive) { controlColor = appearance.channelErroneousTextColorActiveWindow; } else { controlColor = appearance.channelErroneousTextColorInactiveWindow; } // isWindowActive } else { if (isWindowActive) { controlColor = appearance.channelDisabledTextColorActiveWindow; } else { controlColor = appearance.channelDisabledTextColorInactiveWindow; } // isWindowActive } } // isGroupItem NSRange stringValueRange = stringValue.range; if (controlFont) { [mutableStringValue addAttribute:NSFontAttributeName value:controlFont range:stringValueRange]; } if (controlColor) { [mutableStringValue addAttribute:NSForegroundColorAttributeName value:controlColor range:stringValueRange]; } [mutableStringValue endEditing]; return mutableStringValue; } #pragma mark - #pragma mark Badge Drawing - (void)populateMessageCountBadge { TVCServerListAppearance *appearance = self.userInterfaceObjects; TVCServerListCellDrawingContext *drawingContext = self.drawingContext; [self populateMessageCountBadgeWithAppearance:appearance inContext:drawingContext]; } - (void)populateMessageCountBadgeWithAppearance:(TVCServerListAppearance *)appearance inContext:(TVCServerListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isSelected = drawingContext.isSelected; BOOL isSelectedFrontmost = drawingContext.isSelectedFrontmost; BOOL isWindowActive = drawingContext.isWindowActive; BOOL multipleRowsSelected = (self.serverList.numberOfSelectedRows > 1); IRCChannel *associatedChannel = (id)self.cellItem; BOOL drawMessageBadge = (isSelected == NO || (isSelectedFrontmost == NO && isSelected && multipleRowsSelected) || (isWindowActive == NO && isSelected)); if (associatedChannel.config.showTreeBadgeCount == NO) { drawMessageBadge = NO; } NSUInteger treeUnreadCount = associatedChannel.treeUnreadCount; NSUInteger nicknameHighlightCount = associatedChannel.nicknameHighlightCount; BOOL isHighlight = (nicknameHighlightCount > 0); if (associatedChannel.config.ignoreHighlights) { isHighlight = NO; } /* Begin draw if we want to. */ if (treeUnreadCount > 0 && drawMessageBadge) { NSAttributedString *stringToDraw = [self messageCountBadgeTextForCount:treeUnreadCount isHighlight:isHighlight withAppearance:appearance inContext:drawingContext]; NSRect badgeRect = [self messageCountBadgeRectForText:stringToDraw withAppearance:appearance inContext:drawingContext]; [self drawMessageCountBadgeWithString:stringToDraw inRect:badgeRect isHighlight:isHighlight withAppearance:appearance inContext:drawingContext]; self.messageCountBadgeLeadingConstraint.active = YES; self.messageCountBadgeTrailingConstraint.active = YES; } else { self.messageCountBadgeImageView.image = nil; /* Disable constraints when badge is not visible to allow text field to hug the right of the table view. */ self.messageCountBadgeLeadingConstraint.active = NO; self.messageCountBadgeTrailingConstraint.active = NO; } } - (NSAttributedString *)messageCountBadgeTextForCount:(NSUInteger)messageCount isHighlight:(BOOL)isHighlight withAppearance:(TVCServerListAppearance *)appearance inContext:(TVCServerListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isSelected = drawingContext.isSelected; BOOL isWindowActive = drawingContext.isWindowActive; NSString *messageCountString = TXFormattedNumber(messageCount); NSFont *controlFont = nil; if (isSelected) { controlFont = appearance.unreadBadgeFontSelected; } else { controlFont = appearance.unreadBadgeFont; } // isSelected NSColor *controlColor = nil; if (isSelected) { if (isWindowActive) { controlColor = appearance.unreadBadgeSelectedTextColorActiveWindow; } else { controlColor = appearance.unreadBadgeSelectedTextColorInactiveWindow; } // isWindowActive } else if (isHighlight) { if (isWindowActive) { controlColor = appearance.unreadBadgeHighlightTextColorActiveWindow; } else { controlColor = appearance.unreadBadgeHighlightTextColorInactiveWindow; } // isWindowActive } else { if (isWindowActive) { controlColor = appearance.unreadBadgeTextColorActiveWindow; } else { controlColor = appearance.unreadBadgeTextColorInactiveWindow; } // isWindowActive } NSDictionary *attributes = @{NSForegroundColorAttributeName : controlColor, NSFontAttributeName : controlFont}; NSAttributedString *stringToDraw = [NSAttributedString attributedStringWithString:messageCountString attributes:attributes]; return stringToDraw; } - (NSRect)messageCountBadgeRectForText:(NSAttributedString *)stringToDraw withAppearance:(TVCServerListAppearance *)appearance inContext:(TVCServerListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); CGFloat messageCountWidth = (stringToDraw.size.width + (appearance.unreadBadgePadding * 2.0)); NSRect badgeFrame = NSMakeRect(0.0, 0.0, messageCountWidth, appearance.unreadBadgeHeight); CGFloat minimumWidth = appearance.unreadBadgeMinimumWidth; if (badgeFrame.size.width < minimumWidth) { CGFloat widthDiff = (minimumWidth - badgeFrame.size.width); badgeFrame.size.width += widthDiff; badgeFrame.origin.x -= widthDiff; } return badgeFrame; } - (void)drawMessageCountBadgeWithString:(NSAttributedString *)stringToDraw inRect:(NSRect)rectToDraw isHighlight:(BOOL)isHighlight withAppearance:(TVCServerListAppearance *)appearance inContext:(TVCServerListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isSelected = drawingContext.isSelected; BOOL isWindowActive = drawingContext.isWindowActive; /* Create image that we will draw into. */ NSRect badgeFrame = NSMakeRect(0.0, 0.0, NSWidth(rectToDraw), NSHeight(rectToDraw)); NSImage *badgeImage = [NSImage newImageWithSize:NSMakeSize(NSWidth(rectToDraw), NSHeight(rectToDraw))]; [badgeImage lockFocus]; /* Draw the background color. */ NSColor *backgroundColor = nil; if (isSelected) { if (isWindowActive) { backgroundColor = appearance.unreadBadgeSelectedBackgroundColorActiveWindow; } else { backgroundColor = appearance.unreadBadgeSelectedBackgroundColorInactiveWindow; } // isWindowActive } else if (isHighlight) { NSColor *customColor = appearance.unreadBadgeHighlightBackgroundColorByUser; if (customColor && [customColor isEqual:[NSColor clearColor]] == NO) { backgroundColor = customColor; } else { if (isWindowActive) { backgroundColor = appearance.unreadBadgeHighlightBackgroundColorActiveWindow; } else { backgroundColor = appearance.unreadBadgeHighlightBackgroundColorInactiveWindow; } // isWindowActive } // custom color set } else { if (isWindowActive) { backgroundColor = appearance.unreadBadgeBackgroundColorActiveWindow; } else { backgroundColor = appearance.unreadBadgeBackgroundColorActiveWindow; } // isWindowActive } /* Draw the background of the badge */ NSBezierPath *badgePath = [NSBezierPath bezierPathWithRoundedRect:badgeFrame xRadius:7.0 yRadius:7.0]; [backgroundColor set]; [badgePath fill]; /* Center the text relative to the badge itself */ NSPoint badgeTextPoint = NSMakePoint((NSMidX(badgeFrame) - (stringToDraw.size.width / 2.0)), (NSMidY(badgeFrame) - (stringToDraw.size.height / 2.0))); /* Perform draw and set image */ [stringToDraw drawAtPoint:badgeTextPoint]; [badgeImage unlockFocus]; self.messageCountBadgeImageView.image = badgeImage; } #pragma mark - #pragma mark Cell Information - (BOOL)isGroupItem { return [self isKindOfClass:[TVCServerListCellGroupItem class]]; } - (__kindof TVCServerListRowCell *)rowCell { return (id)self.superview; } - (IRCTreeItem *)cellItem { return self.objectValue; } - (TVCServerList *)serverList { return self.rowCell.serverList; } - (TVCServerListAppearance *)userInterfaceObjects { return self.rowCell.userInterfaceObjects; } - (TVCServerListCellDrawingContext *)drawingContext { TVCServerList *serverList = self.serverList; TVCServerListAppearance *appearance = self.userInterfaceObjects; IRCTreeItem *cellItem = self.cellItem; NSInteger rowIndex = [serverList rowForItem:cellItem]; TVCMainWindow *mainWindow = self.mainWindow; TVCServerListCellDrawingContext *drawingContext = [TVCServerListCellDrawingContext new]; drawingContext.isActive = cellItem.isActive; drawingContext.isGroupItem = self.isGroupItem; drawingContext.isInverted = appearance.isDarkAppearance; drawingContext.isSelected = [serverList isRowSelected:rowIndex]; drawingContext.isSelectedFrontmost = [mainWindow isItemSelected:cellItem]; drawingContext.isWindowActive = mainWindow.isActiveForDrawing; return drawingContext; } - (BOOL)needsDisplayWhenApplicationAppearanceChanges { return NO; } - (BOOL)needsDisplayWhenSystemAppearanceChanges { return NO; } @end @implementation TVCServerListCellGroupItem - (void)defineConstraints { TVCServerListAppearance *appearance = self.userInterfaceObjects; self.cellTextFieldLeftMarginConstraint.constant = appearance.serverLabelLeftMargin; } - (void)applicationAppearanceChanged { [super defineConstraints]; [self defineConstraints]; } @end @implementation TVCServerListCellChildItem @end @implementation TVCServerListCellDrawingContext @end #pragma mark - #pragma mark Row Cell @implementation TVCServerListRowCell - (instancetype)initWithServerList:(TVCServerList *)serverList { if ((self = [super initWithFrame:NSZeroRect])) { self.serverList = serverList; return self; } return nil; } - (void)drawDraggingDestinationFeedbackInRect:(NSRect)dirtyRect { ; // Do nothing for this... } - (void)setSelected:(BOOL)selected { super.selected = selected; if (selected == NO && self.invalidatingBackgroundForSelection) { return; } [self setNeedsDisplayOnChild]; } - (void)setNeedsDisplayOnChild { self.childCell.needsDisplay = YES; } - (void)drawSelectionInRect:(NSRect)dirtyRect { if ([self needsToDrawRect:dirtyRect] == NO) { return; } BOOL isWindowActive = self.mainWindow.isActiveForDrawing; TVCServerListAppearance *appearance = self.userInterfaceObjects; NSColor *selectionColor = nil; if (isWindowActive) { selectionColor = appearance.rowSelectionColorActiveWindow; } else { selectionColor = appearance.rowSelectionColorInactiveWindow; } // isWindowActive if (selectionColor) { [selectionColor set]; NSRect selectionRect = self.bounds; NSRectFill(selectionRect); } else { [super drawSelectionInRect:dirtyRect]; } // selectionColor } - (void)didAddSubview:(NSView *)subview { TVCServerListCellGroupItem *childCell = self.childCell; [childCell defineConstraints]; [super didAddSubview:subview]; } #pragma mark - #pragma mark Cell Information - (BOOL)isEmphasized { TVCServerListAppearance *appearance = self.userInterfaceObjects; BOOL emphasized = NO; if (self.isGroupItem) { emphasized = appearance.serverRowEmphasized; } else { emphasized = appearance.channelRowEmphasized; } NSWindow *window = self.window; return (emphasized && (window == nil || window.isKeyWindow)); } - (__kindof TVCServerListCell * _Nullable)childCell { if (self->_childCell == nil) { if (self.numberOfColumns == 0) { return nil; } self->_childCell = [self viewAtColumn:0]; } return self->_childCell; } - (TVCServerListAppearance *)userInterfaceObjects { return self.serverList.userInterfaceObjects; } - (BOOL)isGroupItem { return [self isKindOfClass:[TVCServerListGroupRowCell class]]; } @end @implementation TVCServerListGroupRowCell @end @implementation TVCServerListChildRowCell @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/TVCAppearance.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TVCAppearancePrivate.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TVCListAppearanceColorType) { TVCListAppearanceColorTypeCalibratedWhite = 1, // [alpha] TVCListAppearanceColorTypeRGB = 2, // [alpha] TVCListAppearanceColorTypeSystem = 3, // selector }; typedef NS_ENUM(NSUInteger, TVCListAppearanceImageType) { TVCListAppearanceImageTypeAsset = 1, }; @interface TVCAppearance () @property (nonatomic, assign, readwrite) BOOL isHighResolutionAppearance; @property (nonatomic, copy, nullable, readwrite) NSDictionary *appearanceProperties; @end @implementation TVCAppearance - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (nullable instancetype)initWithAppearanceNamed:(NSString *)appearanceName atURL:(NSURL *)appearanceLocation forRetinaDisplay:(BOOL)forRetinaDisplay { NSParameterAssert(appearanceName != nil); NSParameterAssert(appearanceLocation != nil); if ((self = [super init])) { if ([self _loadAppearanceNamed:appearanceName atURL:appearanceLocation forRetinaDisplay:forRetinaDisplay] == NO) { return nil; } self.isHighResolutionAppearance = forRetinaDisplay; return self; } return nil; } - (BOOL)_loadAppearanceNamed:(NSString *)appearanceName atURL:(NSURL *)appearanceLocation forRetinaDisplay:(BOOL)forRetinaDisplay { NSParameterAssert(appearanceName != nil); NSParameterAssert(appearanceLocation != nil); /* Load file */ NSDictionary *appearances = [NSDictionary dictionaryWithContentsOfURL:appearanceLocation]; if (appearances == nil) { return NO; } /* Find the appearance */ NSDictionary *appearance = [appearances dictionaryForKey:appearanceName]; if (appearance == nil) { return NO; } /* Combine any appearances it may inherit from */ appearance = [self.class _combineAppearance:appearance withOtherAppearances:appearances]; if (appearance == nil) { return NO; } /* Save appearance */ self.appearanceProperties = appearance; return YES; } - (void)flushAppearanceProperties { self.appearanceProperties = nil; } #pragma mark - #pragma mark Inheritance /* TVCListAppearance allows for one appearance to inherit properties from another group recursively. */ /* For example: AppearanceDarkRetina -> AppearanceDarkBase -> AppearanceBase */ /* -_combineAppearance:withOtherAppearances: is the staging ground for this logic. The first argument is the properties for the appearance that was specified in -init. The second argument is the contents of the file that appearance originated from. */ + (nullable NSDictionary *)_combineAppearance:(NSDictionary *)appearanceIn withOtherAppearances:(NSDictionary *)otherAppearances { NSParameterAssert(appearanceIn != nil); NSParameterAssert(otherAppearances != nil); /* Object can be an array of strings or a string. */ id inheritedName = [appearanceIn objectForKey:@"inheritFrom"]; if (inheritedName == nil) { return appearanceIn; } /* The array of references is the dictionaries that will be combined. */ __block NSMutableArray *> *inheritedProperties = [NSMutableArray array]; typedef void (^inheritLogicType)(id); __weak __block inheritLogicType inheritLogicWeak = nil; inheritLogicType inheritLogic = ^(id inheritedName) { NSDictionary *lastInheritance = nil; /* Allow for multiple inheritance */ if ([inheritedName isKindOfClass:[NSArray class]]) { for (id name in ((NSArray *)inheritedName).reverseObjectEnumerator) { inheritLogicWeak(name); } return; } else if ([inheritedName isKindOfClass:[NSString class]]) { lastInheritance = [otherAppearances dictionaryForKey:inheritedName]; } /* No inheritance */ if (lastInheritance == nil) { return; } /* Add the properties to the beginning of the array so that we don't need to use a revers enumerator. */ [inheritedProperties insertObject:lastInheritance atIndex:0]; /* Is there deeper inheritance? */ id nextInheritance = [lastInheritance objectForKey:@"inheritFrom"]; if (nextInheritance == nil) { return; } inheritLogicWeak(nextInheritance); }; /* Inherit from the first appearance. */ inheritLogicWeak = inheritLogic; inheritLogic(inheritedName); /* If nothing was inherited, return original input. */ if (inheritedProperties == nil) { return appearanceIn; } /* Blocks used for combining properties */ /* I may over engineered this */ typedef void (^mergingLogicType)(NSMutableDictionary *, NSDictionary *); __weak __block mergingLogicType mergingLogicWeak = nil; NSDictionary *(^mergeImmutableDictionary)(NSDictionary *, NSDictionary *) = ^NSDictionary *(NSDictionary *firstDictionary, NSDictionary *secondDictionary) { NSMutableDictionary *mutableFirstDictionary = [firstDictionary mutableCopy]; mergingLogicWeak(mutableFirstDictionary, secondDictionary); return [mutableFirstDictionary copy]; }; // mergeImmutableDictionary mergingLogicType mergingLogic = ^(NSMutableDictionary *localDictionary, NSDictionary *remoteDictionary) { [remoteDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id remoteObject, BOOL *stop) { id localObject = localDictionary[key]; if (localObject == nil) { [localDictionary setObject:remoteObject forKey:key]; return; } /* I tried to be clever by checking checking if localObject is kind of class NSMutableDictionary so we can call this block recursively on it instead of creating new mutable copy. Yeah, I will never make that mistake. Cluster classes take a crap on -isKindOfClass: */ if ([remoteObject isKindOfClass:[NSDictionary class]] && [localObject isKindOfClass:[NSDictionary class]]) { localObject = mergeImmutableDictionary(localObject, remoteObject); } else { localObject = remoteObject; } [localDictionary setObject:localObject forKey:key]; }]; }; // mergingLogic mergingLogicWeak = mergingLogic; /* Combine properties */ NSMutableDictionary *appearanceOut = [NSMutableDictionary dictionary]; for (NSDictionary *properties in inheritedProperties) { mergingLogic(appearanceOut, properties); } /* Add top most appearance as final combination. */ mergingLogic(appearanceOut, appearanceIn); [appearanceOut removeObjectForKey:@"inheritFrom"]; return [appearanceOut copy]; } #pragma mark - #pragma mark Utilities - (nullable id)_valueForKey:(NSString *)key expectedType:(Class)expectedType { NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self _valueInGroup:group withKey:key expectedType:expectedType]; } - (nullable id)_valueInGroup:(NSDictionary *)group withKey:(NSString *)key expectedType:(Class)expectedType { NSParameterAssert(group != nil); NSParameterAssert(key != nil); id referenceObject = nil; if (self.isHighResolutionAppearance == NO) { referenceObject = group[key]; } else { NSString *retinaKey = [key stringByAppendingString:@"@2x"]; referenceObject = ((group[retinaKey]) ?: group[key]); } if (referenceObject == nil || [referenceObject isKindOfClass:expectedType] == NO) { return nil; } return referenceObject; } #pragma mark - #pragma mark Color - (nullable NSColor *)colorForKey:(NSString *)key { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self colorInGroup:group withKey:key]; } - (nullable NSColor *)colorInGroup:(NSDictionary *)group withKey:(NSString *)key { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSDictionary *colorProperties = [self _valueInGroup:group withKey:key expectedType:[NSDictionary class]]; if (colorProperties == nil) { return nil; } return [self _colorWithProperties:colorProperties]; } - (nullable NSColor *)colorForKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self colorInGroup:group withKey:key forActiveWindow:forActiveWindow]; } - (nullable NSColor *)colorInGroup:(NSDictionary *)group withKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSDictionary *referenceObject = [self _valueInGroup:group withKey:key expectedType:[NSDictionary class]]; if (referenceObject == nil) { return nil; } NSString *colorKey = ((forActiveWindow) ? @"activeWindow" : @"inactiveWindow"); NSDictionary *colorProperties = [referenceObject dictionaryForKey:colorKey]; if (colorProperties == nil) { return nil; } return [self _colorWithProperties:colorProperties]; } - (nullable NSColor *)_colorWithProperties:(NSDictionary *)colorProperties { NSParameterAssert(colorProperties != nil); NSString *colorValue = [colorProperties stringForKey:@"value"]; if (colorValue == nil) { return nil; } TVCListAppearanceColorType colorType = [colorProperties unsignedIntegerForKey:@"type"]; switch (colorType) { case TVCListAppearanceColorTypeCalibratedWhite: { NSArray *components = [colorValue componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (components.count == 0) { return nil; } CGFloat white = [components doubleAtIndex:0]; CGFloat alpha = 1.0; if (components.count == 2) { alpha = [components doubleAtIndex:1]; } return [NSColor colorWithCalibratedWhite:white alpha:alpha]; } case TVCListAppearanceColorTypeRGB: { NSArray *components = [colorValue componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (components.count < 3) { return nil; } CGFloat red = [components doubleAtIndex:0]; CGFloat green = [components doubleAtIndex:1]; CGFloat blue = [components doubleAtIndex:2]; CGFloat alpha = 1.0; if (components.count == 4) { alpha = [components doubleAtIndex:3]; } return [NSColor calibratedColorWithRed:red green:green blue:blue alpha:alpha]; } case TVCListAppearanceColorTypeSystem: { SEL selector = NSSelectorFromString(colorValue); if ([NSColor respondsToSelector:selector] == NO) { LogToConsoleError("Missing color: %{public}@", colorValue); return nil; } return [NSColor performSelector:selector]; } } // switch() return nil; } #pragma mark - #pragma mark Gradient - (nullable NSGradient *)gradientForKey:(NSString *)key { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self gradientInGroup:group withKey:key]; } - (nullable NSGradient *)gradientInGroup:(NSDictionary *)group withKey:(NSString *)key { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSArray *gradientColors = [self _valueInGroup:group withKey:key expectedType:[NSArray class]]; if (gradientColors == nil) { return nil; } return [self _gradientWithColors:gradientColors]; } - (nullable NSGradient *)gradientForKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self gradientInGroup:group withKey:key forActiveWindow:forActiveWindow]; } - (nullable NSGradient *)gradientInGroup:(NSDictionary *)group withKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSDictionary *referenceObject = [self _valueInGroup:group withKey:key expectedType:[NSDictionary class]]; if (referenceObject == nil) { return nil; } NSString *gradientKey = ((forActiveWindow) ? @"activeWindow" : @"inactiveWindow"); NSArray *gradientColors = [referenceObject arrayForKey:gradientKey]; if (gradientColors == nil) { return nil; } return [self _gradientWithColors:gradientColors]; } - (nullable NSGradient *)_gradientWithColors:(NSArray *> *)gradientColorsIn { NSParameterAssert(gradientColorsIn != nil); NSMutableArray *gradientColorsOut = nil; for (NSDictionary *color in gradientColorsIn) { NSColor *colorObject = [self _colorWithProperties:color]; if (colorObject == nil) { continue; } if (gradientColorsOut == nil) { gradientColorsOut = [NSMutableArray array]; } [gradientColorsOut addObject:colorObject]; } if (gradientColorsOut == nil || gradientColorsOut.count < 2) { return nil; } return [[NSGradient alloc] initWithColors:gradientColorsOut]; } #pragma mark - #pragma mark Font - (nullable NSFont *)fontForKey:(NSString *)key { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self fontInGroup:group withKey:key]; } - (nullable NSFont *)fontInGroup:(NSDictionary *)group withKey:(NSString *)key { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSDictionary *fontProperties = [self _valueInGroup:group withKey:key expectedType:[NSDictionary class]]; if (fontProperties == nil) { return nil; } return [self _fontWithProperties:fontProperties]; } - (nullable NSFont *)fontForKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self fontInGroup:group withKey:key forActiveWindow:forActiveWindow]; } - (nullable NSFont *)fontInGroup:(NSDictionary *)group withKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSDictionary *referenceObject = [self _valueInGroup:group withKey:key expectedType:[NSDictionary class]]; if (referenceObject == nil) { return nil; } NSString *fontKey = ((forActiveWindow) ? @"activeWindow" : @"inactiveWindow"); NSDictionary *fontProperties = [referenceObject dictionaryForKey:fontKey]; if (fontProperties == nil) { return nil; } return [self _fontWithProperties:fontProperties]; } - (nullable NSFont *)_fontWithProperties:(NSDictionary *)fontProperties { NSParameterAssert(fontProperties != nil); NSString *name = [fontProperties stringForKey:@"name"]; CGFloat size = [fontProperties doubleForKey:@"size"]; /* Minimum font size is 5 points */ if (name == nil || size < 5.0) { return nil; } CGFloat weight = [fontProperties doubleForKey:@"weight"]; if ([name isEqualToString:@"System"]) { return [NSFont systemFontOfSize:size weight:weight]; } else if ([name isEqualToString:@"SystemBold"]) { return [NSFont boldSystemFontOfSize:size]; } else if ([name isEqualToString:@"SystemMonospace"]) { return [NSFont monospacedDigitSystemFontOfSize:size weight:weight]; } else if ([name isEqualToString:@"SystemMonospaceBold"]) { return [NSFont monospacedDigitSystemFontOfSize:size weight:NSFontWeightBold]; } if (weight > 0) { return [[NSFontManager sharedFontManager] fontWithFamily:name traits:0 weight:weight size:size]; } else { return [NSFont fontWithName:name size:size]; } } #pragma mark - #pragma mark Image - (nullable NSImage *)imageForKey:(NSString *)key { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self imageInGroup:group withKey:key]; } - (nullable NSImage *)imageInGroup:(NSDictionary *)group withKey:(NSString *)key { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSDictionary *imageProperties = [self _valueInGroup:group withKey:key expectedType:[NSDictionary class]]; if (imageProperties == nil) { return nil; } return [self _imageWithProperties:imageProperties]; } - (nullable NSImage *)imageForKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return nil; } return [self imageInGroup:group withKey:key forActiveWindow:forActiveWindow]; } - (nullable NSImage *)imageInGroup:(NSDictionary *)group withKey:(NSString *)key forActiveWindow:(BOOL)forActiveWindow { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSDictionary *referenceObject = [self _valueInGroup:group withKey:key expectedType:[NSDictionary class]]; if (referenceObject == nil) { return nil; } NSString *imageKey = ((forActiveWindow) ? @"activeWindow" : @"inactiveWindow"); NSDictionary *imageProperties = [referenceObject dictionaryForKey:imageKey]; if (imageProperties == nil) { return nil; } return [self _imageWithProperties:imageProperties]; } - (nullable NSImage *)_imageWithProperties:(NSDictionary *)imageProperties { NSParameterAssert(imageProperties != nil); id imageValue = [imageProperties stringForKey:@"value"]; if (imageValue == nil) { return nil; } TVCListAppearanceImageType imageType = [imageProperties unsignedIntegerForKey:@"type"]; switch (imageType) { case TVCListAppearanceImageTypeAsset: { return [NSImage imageNamed:imageValue]; } } return nil; } #pragma mark - #pragma mark Size - (NSSize)sizeForKey:(NSString *)key { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return NSZeroSize; } return [self sizeInGroup:group withKey:key]; } - (NSSize)sizeInGroup:(NSDictionary *)group withKey:(NSString *)key { NSDictionary *referenceObject = [self _valueInGroup:group withKey:key expectedType:[NSDictionary class]]; if (referenceObject == nil) { return NSZeroSize; } CGFloat width = [referenceObject doubleForKey:@"width"]; CGFloat height = [referenceObject doubleForKey:@"height"]; return NSMakeSize(width, height); } #pragma mark - #pragma mark Measurement - (CGFloat)measurementForKey:(NSString *)key { NSParameterAssert(key != nil); NSDictionary *group = self.appearanceProperties; if (group == nil) { return 0; } return [self measurementInGroup:group withKey:key]; } - (CGFloat)measurementInGroup:(NSDictionary *)group withKey:(NSString *)key { NSParameterAssert(group != nil); NSParameterAssert(key != nil); NSNumber *referenceObject = [self _valueInGroup:group withKey:key expectedType:[NSNumber class]]; if (referenceObject == nil) { return 0; } return referenceObject.doubleValue; } @end #pragma mark - #pragma mark Application Appearance @interface TVCApplicationAppearance () @property (nonatomic, strong) TXAppearancePropertyCollection *applicationProperties; @end @implementation TVCApplicationAppearance DESIGNATED_INITIALIZER_EXCEPTION_BODY_BEGIN - (nullable instancetype)initWithAppearanceNamed:(NSString *)appearanceName atURL:(NSURL *)appearanceLocation forRetinaDisplay:(BOOL)forRetinaDisplay { NSAssert(NO, @"Use -initWithAppearanceAtURL:forRetinaDisplay: instead"); return nil; } DESIGNATED_INITIALIZER_EXCEPTION_BODY_END - (nullable instancetype)initWithAppearanceAtURL:(NSURL *)appearanceLocation forRetinaDisplay:(BOOL)forRetinaDisplay { NSParameterAssert(appearanceLocation != nil); TXAppearancePropertyCollection *applicationProperties = [TXSharedApplication sharedAppearance].properties; NSString *appearanceName = applicationProperties.appearanceName; if ((self = [super initWithAppearanceNamed:appearanceName atURL:appearanceLocation forRetinaDisplay:forRetinaDisplay])) { self.applicationProperties = applicationProperties; return self; } return nil; } - (NSString *)appearanceName { return self.applicationProperties.appearanceName; } - (TXAppearanceType)appearanceType { return self.applicationProperties.appearanceType; } - (NSString *)shortAppearanceDescription { return self.applicationProperties.shortAppearanceDescription; } - (BOOL)isDarkAppearance { return self.applicationProperties.isDarkAppearance; } - (TXAppKitAppearanceTarget)appKitAppearanceTarget { return self.applicationProperties.appKitAppearanceTarget; } - (nullable NSAppearance *)appKitAppearance { return self.applicationProperties.appKitAppearance; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/TVCBasicTableView.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "TVCBasicTableView.h" NS_ASSUME_NONNULL_BEGIN @implementation TVCBasicTableView #pragma mark - #pragma mark Table View - (BOOL)respondsToSelector:(SEL)aSelector { /* AppKit will stop on a response that has copy: but if we have no delegate for this method, then it's best to lie about having it in our class. */ if (aSelector == @selector(copy:)) { return [self.pasteboardDelegate respondsToSelector:@selector(copy:)]; } return class_respondsToSelector(self.class, aSelector); } - (void)copy:(id)sender { /* There is no need for a delegate response check here because it is assumed the only way we can lead to this path is if we responded to the call to -respondsToSelector: */ [self.pasteboardDelegate copy:sender]; } - (nullable NSMenu *)menuForEvent:(NSEvent *)event { if (self.selectedRow < 0 && self.presentMenuForEmptySelection == NO) { return nil; } return self.menu; } - (void)rightMouseDown:(NSEvent *)e { NSInteger rowBeneathMouse = self.rowBeneathMouse; if (rowBeneathMouse >= 0) { if ([self.selectedRowIndexes containsIndex:rowBeneathMouse] == NO) { [self selectItemAtIndex:rowBeneathMouse]; } } [super rightMouseDown:e]; } - (void)textDidEndEditing:(NSNotification *)note { if ([self.textEditingDelegate respondsToSelector:@selector(textDidEndEditing:)]) { [self.textEditingDelegate textDidEndEditing:note]; return; } [super textDidEndEditing:note]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/TVCContentNavigationOutlineView.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "NSViewHelper.h" #import "TVCContentNavigationOutlineViewPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCContentNavigationOutlineView () @property (nonatomic, strong) IBOutlet NSView *contentView; @property (nonatomic, weak, nullable, readwrite) TVCContentNavigationOutlineViewItem *selectedItem; @property (nonatomic, weak, nullable) TVCContentNavigationOutlineViewItem *lastSelection; @property (readonly, nullable) TVCContentNavigationOutlineViewItem *parentOfLastSelection; @end @implementation TVCContentNavigationOutlineView #pragma mark - #pragma mark Basic Class - (void)awakeFromNib { [super awakeFromNib]; self.dataSource = (id)self; self.delegate = (id)self; self.doubleAction = @selector(outlineViewDoubleClicked:); } - (void)setNavigationTreeMatrix:(NSArray *)navigationTreeMatrix { NSParameterAssert(navigationTreeMatrix != nil); if (self->_navigationTreeMatrix != navigationTreeMatrix) { self->_navigationTreeMatrix = navigationTreeMatrix; [self resetOutlineView]; } } - (void)resetOutlineView { self.lastSelection = nil; self.selectedItem = nil; [self reloadData]; } - (void)navigateToItemWithIdentifier:(NSUInteger)identifier { for (TVCContentNavigationOutlineViewItem *groupItem in self.groupItems) { if (groupItem.identifier == identifier) { [self selectItemAtIndex:[self rowForItem:groupItem]]; return; } for (TVCContentNavigationOutlineViewItem *childItem in groupItem.children) { if (childItem.identifier == identifier) { [self selectItemAtIndex:[self rowForItem:childItem]]; return; } } // children } // parents } - (nullable TVCContentNavigationOutlineViewItem *)parentOfLastSelection { TVCContentNavigationOutlineViewItem *selectedItem = self.lastSelection; if (selectedItem == nil) { return nil; } return [self parentForItem:selectedItem]; } #pragma mark - #pragma mark Collapse/Expand Logic - (void)outlineViewDoubleClicked:(id)sender { if (self.expandParentOnDoubleClick == NO) { return; } NSInteger clickedRow = self.clickedRow; if (clickedRow < 0) { return; } TVCContentNavigationOutlineViewItem *itemAtRow = [self itemAtRow:clickedRow]; if (itemAtRow.isGroupItem == NO) { return; } [self expandItem:itemAtRow]; } #pragma mark - #pragma mark NSOutlineViewDelegate Delegates - (NSInteger)outlineView:(NSOutlineView *)sender numberOfChildrenOfItem:(nullable TVCContentNavigationOutlineViewItem *)item { if (item.isGroupItem) { return item.children.count; } return self.navigationTreeMatrix.count; } - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(nullable TVCContentNavigationOutlineViewItem *)item { if (item.isGroupItem) { return item.children[index]; } return self.navigationTreeMatrix[index]; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldCollapseItem:(TVCContentNavigationOutlineViewItem *)item { return YES; } - (BOOL)outlineView:(NSOutlineView *)sender isItemExpandable:(TVCContentNavigationOutlineViewItem *)item { return item.isGroupItem; } - (void)outlineViewItemDidExpand:(NSNotification *)notification { TVCContentNavigationOutlineViewItem *parentItem = self.parentOfLastSelection; TVCContentNavigationOutlineViewItem *itemExpanded = notification.userInfo[@"NSObject"]; if (parentItem == nil || parentItem != itemExpanded) { return; } NSInteger childIndex = [self rowForItem:self.lastSelection]; if (childIndex >= 0) { [self selectItemAtIndex:childIndex]; } } - (nullable id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn byItem:(nullable TVCContentNavigationOutlineViewItem *)item { return item.label; } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(TVCContentNavigationOutlineViewItem *)item { return (item.view != nil); } - (nullable id)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(nullable NSTableColumn *)tableColumn item:(id)item { NSTableCellView *newView = [outlineView makeViewWithIdentifier:@"navEntry" owner:self]; return newView; } - (void)outlineViewSelectionDidChange:(NSNotification *)notification { NSInteger selectedRow = self.selectedRow; if (selectedRow < 0) { /* We do not reset -lastSelection because that is used to restore selection to item when the group item that was collapsed is expanded. */ self.selectedItem = nil; return; } TVCContentNavigationOutlineViewItem *item = [self itemAtRow:selectedRow]; self.selectedItem = item; self.lastSelection = item; [self presentView:item.view]; id firstResponder = item.firstResponder; if (firstResponder) { [self.window makeFirstResponder:firstResponder]; } } - (void)presentView:(NSView *)newView { [self.contentView replaceFirstSubview:newView]; } @end #pragma mark - #pragma mark Item Parent @interface TVCContentNavigationOutlineViewItem () @property (nonatomic, copy, readwrite) NSString *label; @property (nonatomic, assign, readwrite) NSUInteger identifier; @property (nonatomic, weak, nullable, readwrite) NSView *view; @property (nonatomic, weak, nullable, readwrite) NSControl *firstResponder; @property (nonatomic, copy, nullable, readwrite) NSArray *children; @end @implementation TVCContentNavigationOutlineViewItem - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithLabel:(NSString *)label identifier:(NSUInteger)identifier view:(NSView *)view firstResponder:(nullable NSControl *)firstResponder { NSParameterAssert(label != nil); NSParameterAssert(view != nil); return [self initWithLabel:label identifier:identifier view:view firstResponder:firstResponder children:nil]; } - (instancetype)initWithLabel:(NSString *)label identifier:(NSUInteger)identifier view:(nullable NSView *)view firstResponder:(nullable NSControl *)firstResponder children:(nullable NSArray *)children { NSParameterAssert(label != nil); NSParameterAssert(view != nil || children != nil); if ((self = [super init])) { self.label = label; self.identifier = identifier; self.view = view; self.firstResponder = firstResponder; self.children = children; return self; } return nil; } - (BOOL)isGroupItem { return (self.children != nil); } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/TVCDockIcon.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TXMasterController.h" #import "TPCPreferencesLocal.h" #import "IRCClient.h" #import "IRCChannel.h" #import "IRCWorld.h" #import "TVCDockIconPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _badgeSeperationSpace 1.0 @implementation TVCDockIcon static NSInteger _cachedHighlightCount = (-1); static NSInteger _cachedMessageCount = (-1); + (void)updateDockIcon { if ([TPCPreferences displayDockBadge] == NO) { return; } NSUInteger highlightCount = 0; NSUInteger messageCount = 0; for (IRCClient *u in worldController().clientList) { for (IRCChannel *c in u.channelList) { if (c.config.pushNotifications) { messageCount += c.dockUnreadCount; } highlightCount += c.nicknameHighlightCount; } } if (messageCount == 0 && highlightCount == 0) { [self drawWithoutCount]; } else { [self drawWithHighlightCount:highlightCount messageCount:messageCount]; } } + (NSImage *)applicationIcon { /* THIS IS A SECRET!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Birthday icon designed by Alex Sørlie Glomsaas. */ NSCalendar *sysCalendar = [NSCalendar currentCalendar]; NSDateComponents *breakdownInfo = [sysCalendar components:(NSCalendarUnitMonth | NSCalendarUnitDay) fromDate:[NSDate date]]; /* The first public commit of Textual occurred on July, 23, 2010. This is the day that we consider the birthday of the application. */ if (breakdownInfo.month == 7 && breakdownInfo.day == 23) { return [NSImage imageNamed:@"applicationIconBirthday"]; } else { return [NSImage imageNamed:@"NSApplicationIcon"]; } } + (void)resetCachedCount { _cachedMessageCount = (-1); _cachedHighlightCount = (-1); } + (void)drawWithoutCount { if (_cachedHighlightCount == 0 && _cachedMessageCount == 0) { return; } _cachedMessageCount = 0; _cachedHighlightCount = 0; [NSApp setApplicationIconImage:[self applicationIcon]]; } + (void)drawWithHighlightCount:(NSUInteger)highlightCount messageCount:(NSUInteger)messageCount { if (_cachedHighlightCount == highlightCount && _cachedMessageCount == messageCount) { return; } _cachedHighlightCount = highlightCount; _cachedMessageCount = messageCount; if (messageCount > 9999) { messageCount = 9999; } if (highlightCount > 9999) { highlightCount = 9999; } BOOL showRedBadge = (messageCount >= 1); BOOL showGreenBadge = (highlightCount >= 1); /* ////////////////////////////////////////////////////////// */ /* Define Text Drawing Globals */ /* ////////////////////////////////////////////////////////// */ NSSize badgeTextSize = NSZeroSize; NSMutableAttributedString *badgeText = [NSMutableAttributedString alloc]; CGFloat badgeTextFrameCorrection = 2.0; NSDictionary *badgeTextAttributes = @{ NSFontAttributeName : [NSFont fontWithName:@"Helvetica" size:24.0], NSForegroundColorAttributeName : [NSColor whiteColor] }; /* ////////////////////////////////////////////////////////// */ /* Load Drawing Images */ /* ////////////////////////////////////////////////////////// */ NSImage *appIcon = [[self applicationIcon] copy]; NSImage *redBadgeLeft = [NSImage imageNamed:@"DIRedBadgeLeft.png"]; NSImage *redBadgeCenter = [NSImage imageNamed:@"DIRedBadgeCenter.png"]; NSImage *redBadgeRight = [NSImage imageNamed:@"DIRedBadgeRight.png"]; NSImage *greenBadgeLeft = [NSImage imageNamed:@"DIGreenBadgeLeft.png"]; NSImage *greenBadgeCenter = [NSImage imageNamed:@"DIGreenBadgeCenter.png"]; NSImage *greenBadgeRight = [NSImage imageNamed:@"DIGreenBadgeRight.png"]; /* ////////////////////////////////////////////////////////// */ /* Build Scaling Frames */ /* ////////////////////////////////////////////////////////// */ NSRect redBadgeLeftFrame, greenBadgeLeftFrame; NSRect redBadgeRightFrame, greenBadgeRightFrame; NSRect redBadgeCenterFrame, greenBadgeCenterFrame; [appIcon lockFocus]; /* Red Badge Size */ redBadgeRightFrame.size.height = 53.0; redBadgeCenterFrame.size.height = 53.0; redBadgeLeftFrame.size.height = 53.0; redBadgeLeftFrame.size.width = 27.0; redBadgeCenterFrame.size.width = [self badgeCenterTileWidth:messageCount]; redBadgeRightFrame.size.width = 26.0; /* Green Badge Size */ greenBadgeRightFrame.size.height = 53.0; greenBadgeCenterFrame.size.height = 53.0; greenBadgeLeftFrame.size.height = 53.0; greenBadgeLeftFrame.size.width = 27.0; greenBadgeCenterFrame.size.width = [self badgeCenterTileWidth:highlightCount]; greenBadgeRightFrame.size.width = 26.0; /* ////////////////////////////////////////////////////////// */ /* If there is no red badge, then the green one is drawn in the same position of the red at the top right of the icon. The following is the math required to position it correctly relative to the icon. If the red icon does exist in this drawing, then we will update these points of origin later on in the drawing. For now, assume it is at the top. */ /* Green Badge Drawing Position */ greenBadgeLeftFrame.origin = NSMakePoint((appIcon.size.width - (greenBadgeRightFrame.size.width + greenBadgeCenterFrame.size.width + greenBadgeLeftFrame.size.width)), // End X Axis (appIcon.size.height - greenBadgeRightFrame.size.height)); greenBadgeCenterFrame.origin = NSMakePoint((appIcon.size.width - (greenBadgeRightFrame.size.width + greenBadgeCenterFrame.size.width)), // End X Axis (appIcon.size.height - greenBadgeRightFrame.size.height)); greenBadgeRightFrame.origin = NSMakePoint((appIcon.size.width - greenBadgeRightFrame.size.width), // End X Axis (appIcon.size.height - greenBadgeRightFrame.size.height)); /* Update origin if red badge will be drawn */ if (showRedBadge) { greenBadgeLeftFrame.origin.y = (appIcon.size.height - (greenBadgeLeftFrame.size.height + redBadgeLeftFrame.size.height + _badgeSeperationSpace)); greenBadgeCenterFrame.origin.y = (appIcon.size.height - (greenBadgeCenterFrame.size.height + redBadgeCenterFrame.size.height + _badgeSeperationSpace)); greenBadgeRightFrame.origin.y = (appIcon.size.height - (greenBadgeRightFrame.size.height + redBadgeRightFrame.size.height + _badgeSeperationSpace)); } /* Red Badge Drawing Position */ redBadgeLeftFrame.origin = NSMakePoint((appIcon.size.width - (redBadgeRightFrame.size.width + redBadgeCenterFrame.size.width + redBadgeLeftFrame.size.width)), // End X Axis (appIcon.size.height - redBadgeRightFrame.size.height)); redBadgeCenterFrame.origin = NSMakePoint((appIcon.size.width - (redBadgeRightFrame.size.width + redBadgeCenterFrame.size.width)), // End X Axis (appIcon.size.height - redBadgeRightFrame.size.height)); redBadgeRightFrame.origin = NSMakePoint((appIcon.size.width - redBadgeRightFrame.size.width), // End X Axis (appIcon.size.height - redBadgeRightFrame.size.height)); /* ////////////////////////////////////////////////////////// */ /* Draw Badges */ /* ////////////////////////////////////////////////////////// */ /* Red Badge */ if (showRedBadge) { [redBadgeLeft drawInRect:redBadgeLeftFrame fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; [redBadgeCenter drawInRect:redBadgeCenterFrame fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; [redBadgeRight drawInRect:redBadgeRightFrame fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; /* Red Badge Text */ badgeText = [badgeText initWithString:[NSString stringWithInteger:messageCount] attributes:badgeTextAttributes]; badgeTextSize = [badgeText size]; CGFloat redBadgeTotalWidth = (redBadgeLeftFrame.size.width + redBadgeCenterFrame.size.width + redBadgeRightFrame.size.width); CGFloat redBadgeTotalHeight = redBadgeCenterFrame.size.height; CGFloat redBadgeWidthCenter = ((redBadgeTotalWidth - badgeTextSize.width) / 2.0); CGFloat redBadgeHeightCenter = ((redBadgeTotalHeight - badgeTextSize.height) / 2.0); NSPoint badgeTextDrawPath = NSMakePoint((appIcon.size.width - redBadgeTotalWidth + redBadgeWidthCenter), (appIcon.size.height - redBadgeTotalHeight + redBadgeHeightCenter + badgeTextFrameCorrection)); [badgeText drawAtPoint:badgeTextDrawPath]; } if (showGreenBadge) { /* Green Badge */ [greenBadgeLeft drawInRect:greenBadgeLeftFrame fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; [greenBadgeCenter drawInRect:greenBadgeCenterFrame fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; [greenBadgeRight drawInRect:greenBadgeRightFrame fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0]; /* Green Badge Text */ badgeText = [badgeText initWithString:[NSString stringWithInteger:highlightCount] attributes:badgeTextAttributes]; badgeTextSize = [badgeText size]; CGFloat greenBadgeTotalWidth = (greenBadgeLeftFrame.size.width + greenBadgeCenterFrame.size.width + greenBadgeRightFrame.size.width); CGFloat greenBadgeTotalHeight = greenBadgeCenterFrame.size.height; CGFloat greenBadgeWidthCenter = ((greenBadgeTotalWidth - badgeTextSize.width) / 2.0); CGFloat greenBadgeHeightCenter = ((greenBadgeTotalHeight - badgeTextSize.height) / 2.0); NSPoint badgeTextDrawPath = NSMakePoint((appIcon.size.width - greenBadgeTotalWidth + greenBadgeWidthCenter), (appIcon.size.height - greenBadgeTotalHeight + greenBadgeHeightCenter + badgeTextFrameCorrection)); if (showRedBadge) { badgeTextDrawPath.y -= (redBadgeCenterFrame.size.height + _badgeSeperationSpace); } [badgeText drawAtPoint:badgeTextDrawPath]; } /* ////////////////////////////////////////////////////////// */ /* Finish Icon */ /* ////////////////////////////////////////////////////////// */ [appIcon unlockFocus]; [NSApp setApplicationIconImage:appIcon]; } + (CGFloat)badgeCenterTileWidth:(NSUInteger)badgeCount { switch (badgeCount) { case 1 ... 9: { return 1.0; } case 10 ... 99: { return 1.0; } case 100 ... 999: { return 18.0; } case 1000 ... 9999: { return 28.0; } } return 1.0; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/User List/TVCMemberList.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCUser.h" #import "IRCChannelUser.h" #import "IRCChannelMemberListControllerPrivate.h" #import "NSViewHelperPrivate.h" #import "TXMasterController.h" #import "TXMenuControllerPrivate.h" #import "TVCMainWindow.h" #import "TPCPreferencesLocal.h" #import "TVCMemberListCellPrivate.h" #import "TVCMemberListAppearancePrivate.h" #import "TVCMemberListUserInfoPopoverPrivate.h" #import "TVCMemberListPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const TVCMemberListDragType = @"TVCMemberListDragType"; @interface TVCMemberList () @property (nonatomic, strong) id userPopoverTrackingArea; @property (nonatomic, assign) BOOL userPopoverMouseIsInView; @property (nonatomic, assign) BOOL userPopoverTimerIsActive; @property (nonatomic, assign) NSPoint userPopoverLastKnownLocalPoint; @property (nonatomic, assign) NSInteger lastRowShownUserInfoPopover; @property (nonatomic, strong, readwrite) TVCMemberListAppearance *userInterfaceObjects; @property (nonatomic, weak, readwrite) IBOutlet NSVisualEffectView *visualEffectView; @property (nonatomic, strong, readwrite) IBOutlet TVCMemberListUserInfoPopover *memberListUserInfoPopover; @property (nonatomic, strong, readwrite) IBOutlet IRCChannelMemberListController *contentController; @end @implementation TVCMemberList - (void)awakeFromNib { [super awakeFromNib]; [self updateTrackingAreas]; [self registerForDraggedTypes:@[NSFilenamesPboardType]]; } - (void)viewDidMoveToWindow { [super viewDidMoveToWindow]; TVCMainWindow *mainWindow = self.mainWindow; if (mainWindow == nil) { [RZNotificationCenter() removeObserver:self]; return; } [RZNotificationCenter() addObserver:self selector:@selector(windowDidBecomeKey:) name:NSWindowDidBecomeKeyNotification object:mainWindow]; [RZNotificationCenter() addObserver:self selector:@selector(windowDidResignKey:) name:NSWindowDidResignKeyNotification object:mainWindow]; [RZNotificationCenter() addObserver:self selector:@selector(mainWindowRequiresRedraw:) name:TVCMainWindowRedrawSubviewsNotification object:mainWindow]; [RZNotificationCenter() addObserver:self selector:@selector(scrollViewBoundsDidChangeNotification:) name:NSViewBoundsDidChangeNotification object:[self scrollViewContentView]]; } #pragma mark - #pragma mark Utilities - (void)assignToChannel:(nullable IRCChannel *)channel { [self.contentController assignToChannel:channel]; } - (nullable id)itemAtRow:(NSInteger)row { NSParameterAssert(row >= 0); NSArray *rows = self.contentController.arrangedObjects; if (row >= rows.count) { return nil; } return rows[row]; } - (NSInteger)rowForItem:(nullable id)item { if (item == nil) { return (-1); } NSArray *rows = self.contentController.arrangedObjects; NSInteger index = [rows indexOfObjectIdenticalTo:item]; if (index == NSNotFound) { return (-1); } return index; } #pragma mark - #pragma mark Mouse Tracking - (void)updateTrackingAreas { [super updateTrackingAreas]; if (self.userPopoverTrackingArea) { [self removeTrackingArea:self.userPopoverTrackingArea]; } self.userPopoverTrackingArea = [[NSTrackingArea alloc] initWithRect:self.frame options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInActiveApp) owner:self userInfo:nil]; [self addTrackingArea:self.userPopoverTrackingArea]; } - (void)destroyUserInfoPopoverOnWindowKeyChange { [self destroyUserInfoPopover]; // Destroy anything shown } - (void)destroyUserInfoPopover { [self cancelPerformRequestsWithSelector:@selector(popDelayedUserInfoExpansionFrame) object:nil]; self.lastRowShownUserInfoPopover = (-1); self.userPopoverMouseIsInView = NO; self.userPopoverTimerIsActive = NO; self.userPopoverLastKnownLocalPoint = NSZeroPoint; if (self.memberListUserInfoPopover.shown) { [self.memberListUserInfoPopover close]; } } - (void)mouseEntered:(NSEvent *)theEvent { self.userPopoverMouseIsInView = YES; if (self.userPopoverTimerIsActive == NO) { self.userPopoverTimerIsActive = YES; [self performSelector:@selector(popDelayedUserInfoExpansionFrame) withObject:nil afterDelay:1.0]; } } - (void)mouseExited:(NSEvent *)theEvent { [self destroyUserInfoPopover]; } - (void)mouseMoved:(NSEvent *)theEvent { NSPoint localPoint = [self convertPoint:theEvent.locationInWindow fromView:nil]; [self popUserInfoExpansionFrameAtPoint:localPoint ignoreTimerCheck:NO]; } - (void)popUserInfoExpansionFrameAtPoint:(NSPoint)localPoint ignoreTimerCheck:(BOOL)ignoreTimer { self.userPopoverLastKnownLocalPoint = localPoint; if ([XRAccessibility isVoiceOverEnabled]) { return; } if (self.userPopoverTimerIsActive && ignoreTimer == NO) { return; // Only allow the timer to pop it } if (self.window.keyWindow == NO) { return; } NSInteger row = [self rowAtPoint:localPoint]; if (row < 0) { return; } if (self.lastRowShownUserInfoPopover != row) { self.lastRowShownUserInfoPopover = row; id rowView = [self viewAtColumn:0 row:row makeIfNecessary:NO]; [rowView drawWithExpansionFrame]; } } - (void)popDelayedUserInfoExpansionFrame { /* Basically we delay the expansion frame (also known as the popover) by one second from the time the user enters the frame so that if they are just moving the mouse through it to another portion of the window we do not try to show a popover. We only want to show a popover if the user has some intention of being in the list. */ if (self.userPopoverMouseIsInView) { [self popUserInfoExpansionFrameAtPoint:self.userPopoverLastKnownLocalPoint ignoreTimerCheck:YES]; } self.userPopoverTimerIsActive = NO; } #pragma mark - #pragma mark Scroll View - (void)scrollViewBoundsDidChangeNotification:(NSNotification *)notification { if ([TPCPreferences memberListUpdatesUserInfoPopoverOnScroll] == NO) { return; } if (notification.object != [self scrollViewContentView]) { return; } NSPoint mouseLocation = [NSEvent mouseLocation]; NSRect mouseLocationFaked = NSMakeRect(mouseLocation.x, mouseLocation.y, 1.0, 1.0); NSRect remotePoint = [self.window convertRectFromScreen:mouseLocationFaked]; NSPoint localPoint = [self convertPoint:remotePoint.origin fromView:nil]; [self popUserInfoExpansionFrameAtPoint:localPoint ignoreTimerCheck:YES]; } - (id)scrollViewContentView { return self.enclosingScrollView.contentView; } #pragma mark - #pragma mark Drag and Drop - (NSInteger)draggedRow:(id )sender { NSPoint p = [self convertPoint:[sender draggingLocation] fromView:nil]; return [self rowAtPoint:p]; } - (NSArray *)draggedFiles:(id )sender { return [[sender draggingPasteboard] propertyListForType:NSFilenamesPboardType]; } - (NSDragOperation)draggingEntered:(id )sender { return [self draggingUpdated:sender]; } - (NSDragOperation)draggingUpdated:(id )sender { NSArray *files = [self draggedFiles:sender]; if (files.count > 0 && [self draggedRow:sender] >= 0) { return NSDragOperationCopy; } else { return NSDragOperationNone; } } - (BOOL)prepareForDragOperation:(id )sender { NSArray *files = [self draggedFiles:sender]; return (files.count > 0 && [self draggedRow:sender] >= 0); } - (BOOL)performDragOperation:(id )sender { NSArray *files = [self draggedFiles:sender]; if (files.count > 0) { NSInteger row = [self draggedRow:sender]; if (row >= 0) { [menuController() memberSendDroppedFiles:files row:row]; return YES; } } return NO; } #pragma mark - #pragma mark Drawing Updates - (void)refreshAllDrawings { [self refreshAllDrawings:NO]; } - (void)refreshAllDrawings:(BOOL)skipOcclusionCheck { for (NSUInteger i = 0; i < self.numberOfRows; i++) { [self refreshDrawingForRow:i skipOcclusionCheck:skipOcclusionCheck]; } } - (void)refreshDrawingForRows:(NSIndexSet *)rowIndexes { [self refreshDrawingForRows:rowIndexes skipOcclusionCheck:NO]; } - (void)refreshDrawingForRows:(NSIndexSet *)rowIndexes skipOcclusionCheck:(BOOL)skipOcclusionCheck { NSParameterAssert(rowIndexes != nil); [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { [self refreshDrawingForRow:index skipOcclusionCheck:skipOcclusionCheck]; }]; } - (void)refreshDrawingForRow:(NSInteger)rowIndex { [self refreshDrawingForRow:rowIndex skipOcclusionCheck:NO]; } - (void)refreshDrawingForRow:(NSInteger)rowIndex skipOcclusionCheck:(BOOL)skipOcclusionCheck { if (rowIndex < 0) { return; } if (skipOcclusionCheck == NO && self.mainWindow.occluded) { return; } TVCMemberListCell *rowView = [self viewAtColumn:0 row:rowIndex makeIfNecessary:NO]; rowView.needsDisplay = YES; } - (void)refreshDrawingForMember:(IRCChannelUser *)cellItem { NSParameterAssert(cellItem != nil); NSInteger rowIndex = [self rowForItem:cellItem]; [self refreshDrawingForRow:rowIndex]; } - (void)refreshDrawingForChangesToPreference:(NSString *)preferenceKey { static NSDictionary *preferenceMap = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ preferenceMap = @{ @"User List Mode Badge Colors -> +y" : @(IRCUserRankIRCopByMode), @"User List Mode Badge Colors -> +q" : @(IRCUserRankChannelOwner), @"User List Mode Badge Colors -> +a" : @(IRCUserRankSuperOperator), @"User List Mode Badge Colors -> +o" : @(IRCUserRankNormalOperator), @"User List Mode Badge Colors -> +h" : @(IRCUserRankHalfOperator), @"User List Mode Badge Colors -> +v" : @(IRCUserRankVoiced), @"User List Mode Badge Colors -> no mode" : @(IRCUserRankNone) }; }); NSNumber *rank = preferenceMap[preferenceKey]; if (rank == nil) { return; } IRCUserRank rankEnum = rank.unsignedIntegerValue; [self refreshDrawingForMembersWithRank:rankEnum isIRCop:(rankEnum == IRCUserRankIRCopByMode)]; } - (void)refreshDrawingForMembersWithRank:(IRCUserRank)rank isIRCop:(BOOL)isIRCop { TVCMemberListAppearance *appearance = self.userInterfaceObjects; NSArray *rows = self.contentController.arrangedObjects; [rows enumerateObjectsUsingBlock:^(IRCChannelUser *member, NSUInteger index, BOOL *stop) { if ((member.ranks & rank) == 0 && (isIRCop && isIRCop != member.user.isIRCop)) { return; } [appearance invalidateUserMarkBadgeCacheForSymbol:member.mark rank:rank]; [self refreshDrawingForRow:index]; }]; } - (void)drawContextMenuHighlightForRow:(int)row { // Do not draw focus ring ... } - (BOOL)allowsVibrancy { return YES; } - (void)updateVibrancyWithAppearance:(TVCMemberListAppearance *)appearance { NSParameterAssert(appearance != nil); NSVisualEffectView *visualEffectView = self.visualEffectView; if ([TPCPreferences disableSidebarTranslucency]) { visualEffectView.state = NSVisualEffectStateInactive; } else { visualEffectView.state = NSVisualEffectStateFollowsWindowActiveState; } visualEffectView.material = NSVisualEffectMaterialSidebar; } - (void)applicationAppearanceChanged { TVCMemberListAppearance *appearance = self.mainWindow.userInterfaceObjects.memberList; [self _updateAppearance:appearance]; [self invalidateBackgroundForSelection]; [self refreshAllDrawings:YES]; } - (void)systemAppearanceChanged { // [self invalidateBackgroundForSelection]; } - (void)_updateAppearance:(TVCMemberListAppearance *)appearance { NSParameterAssert(appearance != nil); /* We assign a strong reference to these instead of returning the original value every time so that there are no race conditions for when it changes. */ self.userInterfaceObjects = appearance; [self updateVibrancyWithAppearance:appearance]; if (appearance.isDarkAppearance) { self.enclosingScrollView.scrollerKnobStyle = NSScrollerKnobStyleLight; } else { self.enclosingScrollView.scrollerKnobStyle = NSScrollerKnobStyleDark; } self.needsDisplay = YES; } - (void)windowDidBecomeKey:(NSNotification *)notification { [self windowKeyStateChanged:notification]; } - (void)windowDidResignKey:(NSNotification *)notification { [self destroyUserInfoPopoverOnWindowKeyChange]; [self windowKeyStateChanged:notification]; } - (void)windowKeyStateChanged:(NSNotification *)notification { [self respondToRequiresRedraw]; } - (void)mainWindowRequiresRedraw:(NSNotification *)notification { [self respondToRequiresRedraw]; } - (void)respondToRequiresRedraw { [self refreshAllDrawings:YES]; } #pragma mark - #pragma mark Events - (nullable NSMenu *)menuForEvent:(NSEvent *)theEvent { NSInteger rowBeneathMouse = self.rowBeneathMouse; if (rowBeneathMouse >= 0) { if ([self.selectedRowIndexes containsIndex:rowBeneathMouse] == NO) { [self selectItemAtIndex:rowBeneathMouse]; } return menuController().userControlMenu; } return nil; } - (void)keyDown:(NSEvent *)e { if (self.keyDelegate == nil) { return; } switch (e.keyCode) { case 125: // down arrow case 126: // up arrow { [super keyDown:e]; break; } case 123: // left arrow case 124: // right arrow case 116: // page up case 121: // page down { break; } default: { [self.keyDelegate memberListKeyDown:e]; break; } } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/User List/TVCMemberListAppearance.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSColorHelper.h" #import "NSObjectHelperPrivate.h" #import "NSViewHelperPrivate.h" #import "TPCPreferencesUserDefaults.h" #import "TVCAppearancePrivate.h" #import "TVCMainWindow.h" #import "TVCMemberListPrivate.h" #import "TVCMemberListAppearancePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMemberListAppearance () @property (nonatomic, weak, readwrite) TVCMemberList *memberList; @property (nonatomic, assign, readwrite) CGFloat defaultWidth; @property (nonatomic, assign, readwrite) CGFloat minimumWidth; @property (nonatomic, assign, readwrite) CGFloat maximumWidth; @property (nonatomic, copy, nullable, readwrite) NSColor *rowSelectionColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *rowSelectionColorInactiveWindow; #pragma mark - #pragma mark Member Cell @property (nonatomic, assign, readwrite) BOOL cellRowEmphasized; @property (nonatomic, copy, nullable, readwrite) NSColor *cellTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *cellTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *cellAwayTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *cellAwayTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *cellSelectedTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *cellSelectedTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSFont *cellFont; @property (nonatomic, copy, nullable, readwrite) NSFont *cellFontSelected; #pragma mark - #pragma mark Mark Badge @property (nonatomic, assign, readwrite) CGFloat markBadgeLeftMargin; @property (nonatomic, copy, nullable, readwrite) NSColor *markBadgeBackgroundColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *markBadgeBackgroundColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *markBadgeSelectedBackgroundColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *markBadgeSelectedBackgroundColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *markBadgeTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *markBadgeTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *markBadgeSelectedTextColorActiveWindow; @property (nonatomic, copy, nullable, readwrite) NSColor *markBadgeSelectedTextColorInactiveWindow; @property (nonatomic, copy, nullable, readwrite) NSFont *markBadgeFont; @property (nonatomic, copy, nullable, readwrite) NSFont *markBadgeFontSelected; @property (nonatomic, strong, nullable) NSCache *cachedUserMarkBadges; @end @implementation TVCMemberListAppearance #pragma mark - #pragma mark Initialization - (nullable instancetype)initWithMemberList:(TVCMemberList *)memberList inWindow:(TVCMainWindow *)mainWindow { NSParameterAssert(memberList != nil); NSParameterAssert(mainWindow != nil); NSURL *appearanceLocation = [self.class appearanceLocation]; /* Don't access -mainWindow in serverList to get this value because it may not be on a window at the time this is called. */ BOOL forRetinaDisplay = mainWindow.runningInHighResolutionMode; if ((self = [super initWithAppearanceAtURL:appearanceLocation forRetinaDisplay:forRetinaDisplay])) { self.memberList = memberList; [self prepareInitialState]; return self; } return nil; } + (NSURL *)appearanceLocation { return [RZMainBundle() URLForResource:@"TVCMemberListAppearance" withExtension:@"plist"]; } - (void)prepareInitialState { NSDictionary *properties = self.appearanceProperties; self.defaultWidth = [self measurementForKey:@"defaultWidth"]; self.minimumWidth = [self measurementForKey:@"minimumWidth"]; self.maximumWidth = [self measurementForKey:@"maximumWidth"]; self.rowSelectionColorActiveWindow = [self colorForKey:@"selectionColor" forActiveWindow:YES]; self.rowSelectionColorInactiveWindow = [self colorForKey:@"selectionColor" forActiveWindow:NO]; NSDictionary *memberCell = properties[@"Member Cell"]; self.cellRowEmphasized = [memberCell boolForKey:@"rowEmphasized"]; self.cellTextColorActiveWindow = [self colorInGroup:memberCell withKey:@"normalTextColor" forActiveWindow:YES]; self.cellTextColorInactiveWindow = [self colorInGroup:memberCell withKey:@"normalTextColor" forActiveWindow:NO]; self.cellAwayTextColorActiveWindow = [self colorInGroup:memberCell withKey:@"awayTextColor" forActiveWindow:YES]; self.cellAwayTextColorInactiveWindow = [self colorInGroup:memberCell withKey:@"awayTextColor" forActiveWindow:NO]; self.cellSelectedTextColorActiveWindow = [self colorInGroup:memberCell withKey:@"selectedTextColor" forActiveWindow:YES]; self.cellSelectedTextColorInactiveWindow = [self colorInGroup:memberCell withKey:@"selectedTextColor" forActiveWindow:NO]; self.cellFont = [self fontInGroup:memberCell withKey:@"font"]; self.cellFontSelected = [self fontInGroup:memberCell withKey:@"fontSelected"]; NSDictionary *markBadge = properties[@"Mark Badge"]; self.markBadgeLeftMargin = [markBadge floatForKey:@"leftMargin"]; self.markBadgeBackgroundColorActiveWindow = [self colorInGroup:markBadge withKey:@"normalBackgroundColor" forActiveWindow:YES]; self.markBadgeBackgroundColorInactiveWindow = [self colorInGroup:markBadge withKey:@"normalBackgroundColor" forActiveWindow:NO]; self.markBadgeSelectedBackgroundColorActiveWindow = [self colorInGroup:markBadge withKey:@"selectedBackgroundColor" forActiveWindow:YES]; self.markBadgeSelectedBackgroundColorInactiveWindow = [self colorInGroup:markBadge withKey:@"selectedBackgroundColor" forActiveWindow:NO]; self.markBadgeTextColorActiveWindow = [self colorInGroup:markBadge withKey:@"normalTextColor" forActiveWindow:YES]; self.markBadgeTextColorInactiveWindow = [self colorInGroup:markBadge withKey:@"normalTextColor" forActiveWindow:NO]; self.markBadgeSelectedTextColorActiveWindow = [self colorInGroup:markBadge withKey:@"selectedTextColor" forActiveWindow:YES]; self.markBadgeSelectedTextColorInactiveWindow = [self colorInGroup:markBadge withKey:@"selectedTextColor" forActiveWindow:NO]; self.markBadgeFont = [self fontInGroup:markBadge withKey:@"font"]; self.markBadgeFontSelected = [self fontInGroup:markBadge withKey:@"fontSelected"]; [self flushAppearanceProperties]; } #pragma mark - #pragma mark Everything Else - (NSString *)_keyForRetrievingCachedUserMarkBadgeWithSymbol:(NSString *)modeSymbol rank:(IRCUserRank)rank { if (modeSymbol.length == 0) { return [NSString stringWithFormat:@"%lu > (No Rank)", rank]; } else { return [NSString stringWithFormat:@"%lu > %@", rank, modeSymbol]; } } - (nullable NSImage *)cachedUserMarkBadgeForSymbol:(NSString *)modeSymbol rank:(IRCUserRank)rank { NSParameterAssert(modeSymbol != nil); NSCache *cache = self.cachedUserMarkBadges; if (cache == nil) { return nil; } NSString *key = [self _keyForRetrievingCachedUserMarkBadgeWithSymbol:modeSymbol rank:rank]; return [cache objectForKey:key]; } - (void)cacheUserMarkBadge:(NSImage *)badgeImage forSymbol:(NSString *)modeSymbol rank:(IRCUserRank)rank { NSParameterAssert(badgeImage != nil); NSParameterAssert(modeSymbol != nil); NSCache *cache = self.cachedUserMarkBadges; if (cache == nil) { cache = [NSCache new]; self.cachedUserMarkBadges = cache; } NSString *key = [self _keyForRetrievingCachedUserMarkBadgeWithSymbol:modeSymbol rank:rank]; [cache setObject:badgeImage forKey:key]; } - (void)invalidateUserMarkBadgeCacheForSymbol:(NSString *)modeSymbol rank:(IRCUserRank)rank { NSParameterAssert(modeSymbol != nil); NSCache *cache = self.cachedUserMarkBadges; if (cache == nil) { return; } NSString *key = [self _keyForRetrievingCachedUserMarkBadgeWithSymbol:modeSymbol rank:rank]; [cache removeObjectForKey:key]; } - (void)invalidateUserMarkBadgeCaches { NSCache *cache = self.cachedUserMarkBadges; if (cache == nil) { return; } [cache removeAllObjects]; } - (nullable NSColor *)_userMarkBadgeBackgroundColorWithAlphaCorrect:(NSString *)defaultsKey { NSColor *defaultColor = [RZUserDefaults() colorForKey:defaultsKey]; if (defaultColor == nil) { return nil; } return [defaultColor colorWithAlphaComponent:0.7]; } - (NSColor *)markBadgeBackgroundColor_Y // InspIRCd-2.0 { return [self _userMarkBadgeBackgroundColorWithAlphaCorrect:@"User List Mode Badge Colors -> +y"]; } - (NSColor *)markBadgeBackgroundColor_Q { return [self _userMarkBadgeBackgroundColorWithAlphaCorrect:@"User List Mode Badge Colors -> +q"]; } - (NSColor *)markBadgeBackgroundColor_A { return [self _userMarkBadgeBackgroundColorWithAlphaCorrect:@"User List Mode Badge Colors -> +a"]; } - (NSColor *)markBadgeBackgroundColor_O { return [self _userMarkBadgeBackgroundColorWithAlphaCorrect:@"User List Mode Badge Colors -> +o"]; } - (NSColor *)markBadgeBackgroundColor_H { return [self _userMarkBadgeBackgroundColorWithAlphaCorrect:@"User List Mode Badge Colors -> +h"]; } - (NSColor *)markBadgeBackgroundColor_V { return [self _userMarkBadgeBackgroundColorWithAlphaCorrect:@"User List Mode Badge Colors -> +v"]; } - (nullable NSColor *)markBadgeBackgroundColorByUser { return [self _userMarkBadgeBackgroundColorWithAlphaCorrect:@"User List Mode Badge Colors -> no mode"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/User List/TVCMemberListCell.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSStringHelper.h" #import "NSTableViewHelperPrivate.h" #import "NSViewHelperPrivate.h" #import "TLOLocalization.h" #import "TPCPreferencesLocal.h" #import "IRCChannelUser.h" #import "IRCUser.h" #import "TVCMainWindow.h" #import "TVCMemberListAppearance.h" #import "TVCMemberListPrivate.h" #import "TVCMemberListUserInfoPopoverPrivate.h" #import "TVCMemberListCellPrivate.h" NS_ASSUME_NONNULL_BEGIN @class TVCMemberListCellDrawingContext; @interface TVCMemberListRowCell () @property (nonatomic, weak) TVCMemberList *memberList; @property (nonatomic, weak) TVCMemberListCell *childCell; @property (readonly) TVCMemberListAppearance *userInterfaceObjects; @end @interface TVCMemberListCell () @property (nonatomic, weak) IBOutlet NSTextField *cellTextField; @property (readonly, copy) TVCMemberListCellDrawingContext *drawingContext; @property (readonly) TVCMemberList *memberList; @property (readonly) TVCMemberListRowCell *rowCell; @property (readonly) TVCMemberListAppearance *userInterfaceObjects; @property (readonly) IRCChannelUser *cellItem; @property (readonly) NSInteger rowIndex; @property (nonatomic, strong) IBOutlet NSLayoutConstraint *markBadgeLeftMarginConstraint; @end @interface TVCMemberListCellDrawingContext : NSObject @property (nonatomic, assign) BOOL isInverted; @property (nonatomic, assign) BOOL isSelected; @property (nonatomic, assign) BOOL isWindowActive; @end @implementation TVCMemberListCell #pragma mark - #pragma mark Drawing - (void)defineConstraints { TVCMemberListAppearance *appearance = self.userInterfaceObjects; self.markBadgeLeftMarginConstraint.constant = appearance.markBadgeLeftMargin; } - (void)applicationAppearanceChanged { [self defineConstraints]; } - (BOOL)wantsUpdateLayer { return YES; } - (NSViewLayerContentsRedrawPolicy)layerContentsRedrawPolicy { return NSViewLayerContentsRedrawOnSetNeedsDisplay; } - (void)updateLayer { [self updateDrawing]; } - (void)updateDrawing { TVCMemberListCellDrawingContext *drawingContext = self.drawingContext; [self updateTextFieldInContext:drawingContext]; TVCMemberListAppearance *appearance = self.userInterfaceObjects; [self updateDrawingWithAppearance:appearance inContext:drawingContext]; [self updateMarkBadgeWithAppearance:appearance inContext:drawingContext]; } - (void)updateTextFieldInContext:(TVCMemberListCellDrawingContext *)drawingContext { NSParameterAssert(drawingContext != nil); /* Update string value */ IRCChannelUser *cellItem = self.cellItem; NSString *stringValueNew = cellItem.user.nickname; NSTextField *textField = self.cellTextField; NSString *stringValueOld = textField.stringValue; if ([stringValueOld isEqualTo:stringValueNew]) { return; } textField.stringValue = stringValueNew; /* Update accessibility */ NSTextFieldCell *textFieldCell = textField.cell; [textFieldCell setAccessibilityValueDescription:TXTLS(@"Accessibility[alq-6s]", stringValueNew)]; } - (void)updateDrawingWithAppearance:(TVCMemberListAppearance *)appearance inContext:(TVCMemberListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); NSAttributedString *newValue = [self attributedTextFieldValueWithAppearance:appearance inContext:drawingContext]; self.cellTextField.attributedStringValue = newValue; } - (NSAttributedString *)attributedTextFieldValueWithAppearance:(TVCMemberListAppearance *)appearance inContext:(TVCMemberListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isSelected = drawingContext.isSelected; BOOL isWindowActive = drawingContext.isWindowActive; IRCChannelUser *cellItem = self.cellItem; NSTextField *textField = self.cellTextField; NSAttributedString *stringValue = textField.attributedStringValue; NSMutableAttributedString *mutableStringValue = [stringValue mutableCopy]; [mutableStringValue beginEditing]; NSFont *controlFont = nil; if (isSelected) { controlFont = appearance.cellFontSelected; } else { controlFont = appearance.cellFont; } // isSelected NSColor *controlColor = nil; if (isSelected) { if (isWindowActive) { controlColor = appearance.cellSelectedTextColorActiveWindow; } else { controlColor = appearance.cellSelectedTextColorInactiveWindow; } // isWindowActive } else if (cellItem.user.isAway) { if (isWindowActive) { controlColor = appearance.cellAwayTextColorActiveWindow; } else { controlColor = appearance.cellAwayTextColorInactiveWindow; } // isWindowActive } else { if (isWindowActive) { controlColor = appearance.cellTextColorActiveWindow; } else { controlColor = appearance.cellTextColorInactiveWindow; } // isWindowActive } NSRange stringValueRange = stringValue.range; if (controlFont) { [mutableStringValue addAttribute:NSFontAttributeName value:controlFont range:stringValueRange]; } if (controlColor) { [mutableStringValue addAttribute:NSForegroundColorAttributeName value:controlColor range:stringValueRange]; } [mutableStringValue endEditing]; return mutableStringValue; } #pragma mark - #pragma mark Badge Drawing - (NSAttributedString *)markBadgeTextForModeSymbol:(NSString *)modeSymbol isSelected:(BOOL)isSelected withAppearance:(TVCMemberListAppearance *)appearance inContext:(TVCMemberListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isWindowActive = drawingContext.isWindowActive; NSFont *controlFont = nil; if (isSelected) { controlFont = appearance.markBadgeFontSelected; } else { controlFont = appearance.markBadgeFont; } // isSelected NSColor *controlColor = nil; if (isSelected) { if (isWindowActive) { controlColor = appearance.markBadgeSelectedTextColorActiveWindow; } else { controlColor = appearance.markBadgeSelectedTextColorInactiveWindow; } // isWindowActive } else { if (isWindowActive) { controlColor = appearance.markBadgeTextColorActiveWindow; } else { controlColor = appearance.markBadgeTextColorInactiveWindow; } // isWindowActive } // isSelected NSDictionary *attributes = @{NSForegroundColorAttributeName : controlColor, NSFontAttributeName : controlFont}; NSAttributedString *stringToDraw = [NSAttributedString attributedStringWithString:modeSymbol attributes:attributes]; return stringToDraw; } - (void)updateMarkBadgeWithAppearance:(TVCMemberListAppearance *)appearance inContext:(TVCMemberListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isSelected = drawingContext.isSelected; IRCChannelUser *cellItem = self.cellItem; NSString *modeSymbol = cellItem.mark; IRCUserRank userRankToDraw = IRCUserRankNone; if ([TPCPreferences memberListSortFavorsServerStaff]) { if (cellItem.user.isIRCop) { userRankToDraw = IRCUserRankIRCopByMode; } } if (userRankToDraw == IRCUserRankNone) { userRankToDraw = cellItem.rank; } NSImage *cachedImage = nil; if (isSelected == NO) { cachedImage = [appearance cachedUserMarkBadgeForSymbol:modeSymbol rank:userRankToDraw]; } if (cachedImage == nil) { cachedImage = [self drawMarkBadgeForRank:userRankToDraw isSelected:isSelected withAppearance:appearance inContext:drawingContext]; if (isSelected == NO) { [appearance cacheUserMarkBadge:cachedImage forSymbol:modeSymbol rank:userRankToDraw]; } } self.imageView.image = cachedImage; } - (NSImage *)drawMarkBadgeForRank:(IRCUserRank)userRank isSelected:(BOOL)isSelected withAppearance:(TVCMemberListAppearance *)appearance inContext:(TVCMemberListCellDrawingContext *)drawingContext { NSParameterAssert(appearance != nil); NSParameterAssert(drawingContext != nil); BOOL isWindowActive = drawingContext.isWindowActive; /* Create image that we will draw into. */ NSRect imageViewFrame = self.imageView.frame; NSRect badgeFrame = NSMakeRect(0.0, 0.0, imageViewFrame.size.width, imageViewFrame.size.height); NSImage *badgeImage = [NSImage newImageWithSize:NSMakeSize(NSWidth(badgeFrame), NSHeight(badgeFrame))]; [badgeImage lockFocus]; /* Decide the background color */ NSColor *backgroundColor = nil; if (isSelected) { if (isWindowActive) { backgroundColor = appearance.markBadgeSelectedBackgroundColorActiveWindow; } else { backgroundColor = appearance.markBadgeSelectedBackgroundColorInactiveWindow; } // isWindowActive } else if (userRank == IRCUserRankIRCopByMode) { backgroundColor = appearance.markBadgeBackgroundColor_Y; } else if (userRank == IRCUserRankChannelOwner) { backgroundColor = appearance.markBadgeBackgroundColor_Q; } else if (userRank == IRCUserRankSuperOperator) { backgroundColor = appearance.markBadgeBackgroundColor_A; } else if (userRank == IRCUserRankNormalOperator) { backgroundColor = appearance.markBadgeBackgroundColor_O; } else if (userRank == IRCUserRankHalfOperator) { backgroundColor = appearance.markBadgeBackgroundColor_H; } else if (userRank == IRCUserRankVoiced) { backgroundColor = appearance.markBadgeBackgroundColor_V; } else { NSColor *customColor = appearance.markBadgeBackgroundColorByUser; if (customColor && [customColor isEqual:[NSColor clearColor]] == NO) { backgroundColor = customColor; } else { if (isWindowActive) { backgroundColor = appearance.markBadgeBackgroundColorActiveWindow; } else { backgroundColor = appearance.markBadgeBackgroundColorInactiveWindow; } // isWindowActive } // custom color set } /* Set "x" if the user has no modes set */ NSString *stringToDraw = self.cellItem.mark; if ([TPCPreferences memberListDisplayNoModeSymbol]) { if (stringToDraw.length == 0) { stringToDraw = @"×"; } } /* Draw the background of the badge */ NSBezierPath *badgePath = [NSBezierPath bezierPathWithRoundedRect:badgeFrame xRadius:4.0 yRadius:4.0]; [backgroundColor set]; [badgePath fill]; /* Begin building the actual mode string */ if (stringToDraw.length > 0) { NSAttributedString *badgeText = [self markBadgeTextForModeSymbol:stringToDraw isSelected:isSelected withAppearance:appearance inContext:drawingContext]; NSSize badgeTextSize = badgeText.size; NSPoint badgeTextPoint = NSMakePoint((NSMidX(badgeFrame) - (badgeTextSize.width / 2.0)), (NSMidY(badgeFrame) - (badgeTextSize.height / 2.0))); if (appearance.isHighResolutionAppearance) { if ([stringToDraw isEqualToString:@"+"] || [stringToDraw isEqualToString:@"~"] || [stringToDraw isEqualToString:@"×"]) { badgeTextPoint.y += 1.0; } else if ([stringToDraw isEqualToString:@"^"]) { badgeTextPoint.y -= 2.0; } else if ([stringToDraw isEqualToString:@"*"]) { badgeTextPoint.y -= 2.5; } /* else if ([stringToDraw isEqualToString:@"@"] || [stringToDraw isEqualToString:@"!"] || [stringToDraw isEqualToString:@"%"] || [stringToDraw isEqualToString:@"&"] || [stringToDraw isEqualToString:@"#"] || [stringToDraw isEqualToString:@"?"] || [stringToDraw isEqualToString:@"$"]) { badgeTextPoint.y -= 0.0; } */ } else // isDrawingForRetina { if ([stringToDraw isEqualToString:@"+"] || [stringToDraw isEqualToString:@"~"] || [stringToDraw isEqualToString:@"×"]) { badgeTextPoint.y += 2.0; } else if ([stringToDraw isEqualToString:@"@"] || [stringToDraw isEqualToString:@"!"] || [stringToDraw isEqualToString:@"%"] || [stringToDraw isEqualToString:@"&"] || [stringToDraw isEqualToString:@"#"] || [stringToDraw isEqualToString:@"?"]) { badgeTextPoint.y += 1.0; } /* else if ([stringToDraw isEqualToString:@"^"]) { badgeTextPoint.y -= 0.0; } */ else if ([stringToDraw isEqualToString:@"*"]) { badgeTextPoint.y -= 1.0; } else if ([stringToDraw isEqualToString:@"$"]) { badgeTextPoint.y += 1.0; } } [badgeText drawAtPoint:badgeTextPoint]; } [badgeImage unlockFocus]; return badgeImage; } #pragma mark - #pragma mark Expansion Frame - (void)drawWithExpansionFrame { TVCMemberList *memberList = self.memberList; TVCMemberListUserInfoPopover *userInfoPopover = memberList.memberListUserInfoPopover; IRCChannelUser *cellItem = self.cellItem; /* =============================================== */ userInfoPopover.nicknameField.stringValue = cellItem.user.nickname; /* =============================================== */ NSString *hostmaskUsername = cellItem.user.username; if (hostmaskUsername.length == 0) { hostmaskUsername = TXTLS(@"TVCMainWindow[d85-9n]"); } userInfoPopover.usernameField.stringValue = hostmaskUsername; /* =============================================== */ BOOL stripIRCFormatting = [TPCPreferences removeAllFormatting]; NSString *hostmaskAddress = cellItem.user.address; if (hostmaskAddress.length == 0) { hostmaskAddress = TXTLS(@"TVCMainWindow[d85-9n]"); } if (stripIRCFormatting) { userInfoPopover.addressField.stringValue = hostmaskAddress; } else { NSAttributedString *hostmaskAddressFormatted = [hostmaskAddress attributedStringWithIRCFormatting:[NSFont systemFontOfSize:12.0] preferredFontColor:nil honorFormattingPreference:NO]; userInfoPopover.addressField.attributedStringValue = hostmaskAddressFormatted; } /* =============================================== */ NSString *realName = cellItem.user.realName; if (realName.length == 0) { realName = TXTLS(@"TVCMainWindow[d85-9n]"); } if (stripIRCFormatting) { userInfoPopover.realNameField.stringValue = realName; } else { NSAttributedString *realNameFormatted = [realName attributedStringWithIRCFormatting:[NSFont systemFontOfSize:12.0] preferredFontColor:nil honorFormattingPreference:NO]; userInfoPopover.realNameField.attributedStringValue = realNameFormatted; } /* =============================================== */ if (cellItem.user.isAway) { userInfoPopover.awayStatusField.stringValue = TXTLS(@"TVCMainWindow[jkr-ed]"); } else { userInfoPopover.awayStatusField.stringValue = TXTLS(@"TVCMainWindow[gi6-wf]"); } /* =============================================== */ IRCUserRank userRank = cellItem.rank; if (cellItem.user.isIRCop) { userRank = IRCUserRankIRCopByMode; } NSString *userPrivileges = nil; if (userRank == IRCUserRankIRCopByMode) { userPrivileges = TXTLS(@"TVCMainWindow[i8t-vb]"); } else if (userRank == IRCUserRankChannelOwner) { userPrivileges = TXTLS(@"TVCMainWindow[p1z-sc]"); } else if (userRank == IRCUserRankSuperOperator) { userPrivileges = TXTLS(@"TVCMainWindow[som-zo]"); } else if (userRank == IRCUserRankNormalOperator) { userPrivileges = TXTLS(@"TVCMainWindow[0kn-s5]"); } else if (userRank == IRCUserRankHalfOperator) { userPrivileges = TXTLS(@"TVCMainWindow[0nn-te]"); } else if (userRank == IRCUserRankVoiced) { userPrivileges = TXTLS(@"TVCMainWindow[ya1-sk]"); } else { userPrivileges = TXTLS(@"TVCMainWindow[tjj-z2]"); } userInfoPopover.privilegesField.stringValue = userPrivileges; /* =============================================== */ NSInteger rowIndex = [memberList rowForView:self]; NSRect cellFrame = [memberList frameOfCellAtColumn:0 row:rowIndex]; /* Presenting the popover will steal focus. To workaround this, we record the active first responder then set it back. */ NSWindow *window = self.window; NSResponder *activeFirstResponder = window.firstResponder; [userInfoPopover showRelativeToRect:cellFrame ofView:memberList preferredEdge:NSMaxXEdge]; [window makeFirstResponder:activeFirstResponder]; } - (TVCMemberListRowCell *)rowCell { return (id)self.superview; } - (IRCChannelUser *)cellItem { return self.objectValue; } - (TVCMemberList *)memberList { return self.rowCell.memberList; } - (TVCMemberListAppearance *)userInterfaceObjects { return self.rowCell.userInterfaceObjects; } - (TVCMemberListCellDrawingContext *)drawingContext { TVCMemberList *memberList = self.memberList; NSInteger rowIndex = [memberList rowForView:self]; TVCMemberListAppearance *appearance = self.userInterfaceObjects; TVCMemberListCellDrawingContext *drawingContext = [TVCMemberListCellDrawingContext new]; TVCMainWindow *mainWindow = self.mainWindow; drawingContext.isInverted = appearance.isDarkAppearance; drawingContext.isSelected = [memberList isRowSelected:rowIndex]; drawingContext.isWindowActive = mainWindow.isActiveForDrawing; return drawingContext; } @end @implementation TVCMemberListCellDrawingContext @end #pragma mark - #pragma mark Row View Cell @implementation TVCMemberListRowCell - (instancetype)initWithMemberList:(TVCMemberList *)memberList { NSParameterAssert(memberList != nil); if ((self = [super initWithFrame:NSZeroRect])) { self.memberList = memberList; return self; } return nil; } - (void)setSelected:(BOOL)selected { super.selected = selected; if (selected == NO && self.invalidatingBackgroundForSelection) { return; } [self setNeedsDisplayOnChild]; } - (void)setNeedsDisplayOnChild { self.childCell.needsDisplay = YES; } - (void)drawSelectionInRect:(NSRect)dirtyRect { if ([self needsToDrawRect:dirtyRect] == NO) { return; } BOOL isWindowActive = self.mainWindow.isActiveForDrawing; TVCMemberListAppearance *appearance = self.userInterfaceObjects; NSColor *selectionColor = nil; if (isWindowActive) { selectionColor = appearance.rowSelectionColorActiveWindow; } else { selectionColor = appearance.rowSelectionColorInactiveWindow; } // isWindowActive if (selectionColor) { [selectionColor set]; NSRect selectionRect = self.bounds; NSRectFill(selectionRect); } else { [super drawSelectionInRect:dirtyRect]; } // selectionColor } - (void)didAddSubview:(NSView *)subview { TVCMemberListCell *childCell = self.childCell; [childCell defineConstraints]; [super didAddSubview:subview]; } #pragma mark - #pragma mark Cell Information - (BOOL)isEmphasized { TVCMemberListAppearance *appearance = self.userInterfaceObjects; NSWindow *window = self.window; return (appearance.cellRowEmphasized && (window == nil || window.isKeyWindow)); } - (TVCMemberListCell * _Nullable)childCell { if (self->_childCell == nil) { if (self.numberOfColumns == 0) { return nil; } self->_childCell = [self viewAtColumn:0]; } return self->_childCell; } - (TVCMemberListAppearance *)userInterfaceObjects { return self.memberList.userInterfaceObjects; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Classes/Views/User List/TVCMemberListUserInfoPopover.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCMemberListUserInfoPopoverPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TVCMemberListUserInfoPopover () @property (nonatomic, weak, readwrite) IBOutlet NSTextField *nicknameField; @property (nonatomic, weak, readwrite) IBOutlet NSTextField *usernameField; @property (nonatomic, weak, readwrite) IBOutlet NSTextField *addressField; @property (nonatomic, weak, readwrite) IBOutlet NSTextField *realNameField; @property (nonatomic, weak, readwrite) IBOutlet NSTextField *privilegesField; @property (nonatomic, weak, readwrite) IBOutlet NSTextField *awayStatusField; @end @implementation TVCMemberListUserInfoPopover - (void)mouseDown:(NSEvent *)event { [self close]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/App/Configurations/Sandbox/Debug.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.application-groups com.codeux.apps.textual com.codeux.apps.textual.group 8482Q6EPL6.com.codeux.apps.textual 8482Q6EPL6.com.codeux.irc.textual com.apple.security.automation.apple-events com.apple.security.cs.disable-library-validation com.apple.security.files.bookmarks.app-scope com.apple.security.files.downloads.read-write com.apple.security.files.user-selected.read-write com.apple.security.network.client com.apple.security.network.server com.apple.security.print com.apple.security.temporary-exception.apple-events com.apple.itunes com.apple.safari com.apple.security.temporary-exception.shared-preference.read-write com.codeux.apps.textual com.codeux.apps.textual-mas ================================================ FILE: Sources/App/Configurations/Sandbox/Sandbox Disabled.entitlements ================================================ com.apple.security.app-sandbox ================================================ FILE: Sources/App/Configurations/Sandbox/Standard Release.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.application-groups com.codeux.apps.textual com.codeux.apps.textual.group 8482Q6EPL6.com.codeux.apps.textual 8482Q6EPL6.com.codeux.irc.textual com.apple.security.automation.apple-events com.apple.security.cs.disable-library-validation com.apple.security.files.bookmarks.app-scope com.apple.security.files.downloads.read-write com.apple.security.files.user-selected.read-write com.apple.security.network.client com.apple.security.network.server com.apple.security.print com.apple.security.temporary-exception.apple-events com.apple.itunes com.apple.safari com.apple.security.temporary-exception.files.home-relative-path.read-only /Library/Application Support/Textual/Textual_Trial_Information_v2.plist /Library/Application Support/Textual/Textual_User_License_v2.plist com.apple.security.temporary-exception.shared-preference.read-write com.codeux.apps.textual com.codeux.apps.textual-mas com.apple.security.temporary-exception.mach-lookup.global-name $(PRODUCT_BUNDLE_IDENTIFIER)-spks $(PRODUCT_BUNDLE_IDENTIFIER)-spki ================================================ FILE: Sources/App/Resources/Images/Copyright Information for Images.txt ================================================ Please see acknowledgements.pdf for copyright information ================================================ FILE: Sources/App/Resources/Language Files/_randomToken ================================================ #!/usr/bin/php <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "alq-6s" = "User %@ in User List"; "bmy-d2" = "Connection %@, Connected"; "tu4-8u" = "Connection %@, Disconnected"; "75f-og" = "Channel %@, Channel Joined"; "edc-7o" = "Channel %@, Channel Not Joined"; "9sn-xp" = "Query with User %@"; "k79-1a" = "Main Window"; // Main window title description "wbj-gr" = "Alert dialog %@"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/BasicLanguage.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "71l-y7" = "None"; "vbl-xi" = "Unknown"; "1dd-0f" = "Textual IRC Client: www.textualapp.com"; "vfu-c0" = "Untitled Connection"; "zhr-lq" = "My %@ has gone to sleep. ZZZzzz…"; "qi7-5y" = "My Mac has gone to sleep. ZZZzzz…"; "hri-l0" = "Close Query"; "1f6-bg" = "Close Window"; "w3a-je" = "Disconnect from %@"; "5td-3f" = "Leave Channel"; "x97-ro" = "Quit Textual"; "6cw-ni" = "Copy Log as HTML"; "ngd-ms" = "Force Reload Style"; "tfj-m9" = "Open Web Inspector"; "o5l-4s" = "Look Up in Dictionary"; "zxs-yy" = "Look Up “%@”"; "1ll-h9" = "Search With %@"; "eoq-pr-128" = "seconds"; "eoq-pr-64" = "minutes"; "eoq-pr-32" = "hours"; "eoq-pr-16" = "days"; "eoq-pr-8" = "months"; "eoq-pr-4" = "years"; "fko-64-128" = "second"; "fko-64-64" = "minute"; "fko-64-32" = "hour"; "fko-64-16" = "day"; "fko-64-8" = "month"; "fko-64-4" = "year"; "4um-w4" = "%@ ago"; "7kc-mo" = "No Actions Available"; "7lm-bq" = "Built-in"; "bm2-4p" = "Custom"; "fo8-1h" = "Fill out this field"; "iwp-cg" = "IRC Colors"; "ham-vk" = "Color %ld"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/CommonErrors.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "yyx-l3" = "Please enter a properly formatted server address or pick from the list of preconfigured networks."; "l0c-nb" = "Please enter a whole number between 1 and 65,535."; "och-j5" = "Please enter a properly formatted nickname."; "pgl-zg" = "Please enter a properly formatted e-mail address."; "gas-v8" = "Cannot contain more than one line."; "2cb-af" = "Maximum length is %ld characters."; "qi9-i8" = "Minimum length is %ld characters."; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/IRC.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Script errors */ "d3c-9b" = "The command \002%@\002 is defined by a plugin and a script. Ignoring command because of inability to determine priority."; "ax0-mt" = "Script Execution Failure: %@"; "2mc-h0" = "Script Execution Failure (%1$@: %2$@): %3$@"; /* Generic messages */ "0r1-5l" = "Textual has prevented you from banning yourself on %@"; "6rj-2r" = "Failed to send data to server. Not connected."; /* Default G:Line reason */ "56r-wn" = "35d Your behavior is not conducive to the desired environment"; "2aw-rf" = "Your behavior is not conducive to the desired environment"; "yy6-fx" = "Your behavior is not conducive to the desired environment"; "80c-i8" = "1d Shunned"; /* Network not available messages */ "3s6-e6" = "Delaying auto connect for %lu seconds"; /* File transfer server console messages */ "ags-s8" = "Trying file transfer to %1$@, %2$@ (%3$qi bytes)"; "snf-45" = "Received file transfer request from %1$@, %2$@ (%3$qi bytes)"; "y3w-la" = "Textual has received a DCC request from %@ that could not be processed."; /* Version info */ "vzu-u7" = "%1$@ IRC Client: www.textualapp.com — v%2$@"; /* Specific IRC related, app-specific errors */ "iaa-0u" = "There are no servers configured to connect to."; /* Raw mode messages */ "ik6-dl" = "⍅ Server traffic will be output to this window until it is closed by control clicking (right clicking) it. ⍆"; /* Hidden command responses messages */ "yem-td" = "⍅ Command responses which are normally hidden will be output to this window until it is closed by control clicking (right clicking) it. ⍆"; /* Log file session headers */ "qrg-ua" = "------------- Begin Session -------------"; "d5d-uy" = "------------- End Session -------------"; /* Ban list */ "c04-d0-1" = "Ban in %1$@: %2$@ set by %3$@ on %4$@"; "c04-d0-2" = "Ban in %1$@: %2$@"; /* Invite exception list */ "py2-qh-1" = "Invite Exception in %1$@: %2$@ set by %3$@ on %4$@"; "py2-qh-2" = "Invite Exception in %1$@: %2$@"; /* Ban exception list */ "ov2-ci-1" = "Ban exception in %1$@: %2$@ set by %3$@ on %4$@"; "ov2-ci-2" = "Ban exception in %1$@: %2$@"; /* Quiet list */ "u5z-az-1" = "Quiet in %1$@: %2$@ set by %3$@ on %4$@"; "u5z-az-2" = "Quiet in %1$@: %2$@"; /* Cipher suite information */ "uyz-4r" = "\002%1$@\002 with the cipher suite: \002%2$@\002"; "xwj-xy" = "\002%1$@\002 with the cipher suite: \002%2$@\002 \0034\002(deprecated)\002\003"; /* XPC errors */ "vdy-jk" = "Connection service closed unexpectedly"; /* Large messages warning */ "u4c-7i" = "The message that you are sending is very large which means it will be split up into multiple messages. Sending too many messages at once is often considered disruptive and may result in a ban."; "lql-8i" = "Are you sure you want to send this message?"; /* /away/ command */ "41y-p2" = "You have exceeded the maximum away message length for %1$@ which is %2$ld characters. The end of your away message may have been cut off."; /* /defaults/ command */ "1dz-jb" = "Invalid Syntax. Type “/defaults help” for more information."; "bkk-lo" = "The \002defaults\002 command can be used to toggle very specific, rarely used features. Usage: “/defaults enable ” — Enables the feature defined by \002\002 “/defaults disable ” — Disables the feature defined by \002\002 “/defaults features” — View list of supported features Surround \002\002 with double quotes (\") if it contains a space."; "pc4-67" = "Cannot enable the feature “%@” because it is not supported"; "d7y-pv" = "Cannot disable the feature “%@” because it is not supported"; "5ke-18" = "Enabled Feature: %@"; "0gn-cb" = "Disabled Feature: %@"; /* /ignore/ command */ "ret-20" = "Added ignore that matches \002%1$@\002 with pattern: %2$@"; "jzg-g8" = "Removed ignore that matches \002%1$@\002 with pattern: %2$@"; "wu0-jp" = "No ignores could be found that matches \002%@\002"; "5ix-zn" = "An ignore already exists that matches \002%@\002. To modify this ignore, open the \002Address Book\002 using the keyboard shortcut \002Control 3\002."; "vrx-1f" = "Cannot remove ignore for \002%@\002 because Textual isn't sure which to remove. More than one ignore exists that matches this user. To remove a specific ignore, open the \002Address Book\002 using the keyboard shortcut \002Control 3\002."; /* /kick/ command */ "59a-ir" = "You have exceeded the maximum kick message length for %1$@ which is %2$ld characters. The end of your kick message may have been cut off."; /* /lagcheck/ command */ "5bf-jp" = "Received lag check reply from %1$@. Time elapsed: %2$1.0f milliseconds. (Rating: %3$@)"; "qoh-kt" = "Waiting for response from lag check…"; "58g-m9" = "Yeah, okay…"; "0jp-93" = "Are you plugged into the server?"; "yym-8y" = "Pretty good"; "mic-qe" = "Not bad"; "mqg-wi" = "Okay"; "ut8-7s" = "Needs work"; "8fo-ss" = "Slow"; "4oc-p2" = "Very slow"; /* /mode/ command */ "dwi-d1" = "The mode \002+%@\002 is not supported on this server"; /* /monitor/ command errors */ "khw-4y" = "Please use the \002Address Book\002 to add or remove tracked users. Open the \002Address Book\002 using the keyboard shortcut \002Control 3\002."; /* /msg/ command errors */ "54l-h7" = "Cannot send operator message because the mode \002+o\002 is not supported by this server."; /* /mute/ command */ "sdn-yr" = "Sound is already muted"; "190-f2" = "Sound is no longer muted"; "5rf-mj" = "Sound is not muted"; "u48-aa" = "Sound has been muted"; /* /myversion/ command */ "pqj-1y" = "I am using %@"; "ccb-ur" = "%1$@ %2$@ (Build %3$@)%4$@%5$@"; "b8p-44" = " as classic binary on an %@ Mac."; /* /timer/ command */ "jj9-94" = "Invalid Syntax. Type “/timer help” for more information."; "327-pv" = "Timer interval must be a whole number that is grater than zero (0). Type “/timer help” for more information."; "eud-kc" = "Timer repeat count must be a whole number that is grater than or equal to zero (0). Type “/timer help” for more information."; "p6l-o4" = "Timer identifier is not properly formatted. Type “/timer list“ for a list of timers."; "vzu-xh" = "Timer with identifier “%@“ does not exist. Type “/timer list“ for a list of timers."; "uiz-32" = "Timer with identifier “%@“ can't be stopped"; "ax6-n9" = "Timer with identifier “%@“ is already stopped"; "hs0-up" = "Timer with identifier “%@“ stopped"; "dgp-d4" = "Timer with identifier “%@“ can't be restarted. Add a new timer."; "qb7-mi" = "Timer with identifier “%@“ restarted"; "p7s-is" = "Timer with identifier “%@“ removed"; "808-bs" = "All timers removed"; "jef-tp" = "Timer with identifier “%@“ added"; // unused "pqk-5k" = "There are no timers"; "q1m-1e" = "There are %ld timers:"; "6ts-oi" = "There is %ld timer:"; "4n6-2x" = "\002ID:\002 %1$@ • \002Status:\002 %2$@ • \002Interval:\002 %3$@ • \002Next Fire:\002 %4$@ • \002Command:\002 “%5$@“"; "uw0-v2" = "\002ID:\002 %1$@ • \002Status:\002 %2$@ • \002Interval:\002 %3$@ • \002Next Fire:\002 %4$@ • \002Repeat:\002 Yes • \002Repeat Limit:\002 %5$@ • \002Iteration:\002 %6$ld • \002Command:\002 “%7$@“"; "bhz-9e" = "Active"; "ww4-sn" = "Stopped"; "o26-ae" = "No Limit"; "xkq-rt" = "The timer command can be used to perform a command after a specific amount of time and optionally repeat it. To learn more, type: “/timer help “ Available topics: ADD, REMOVE, LIST, STOP, RESTART"; "6r0-il" = "“/timer “ \002\002 is the interval of the timer. The interval must be a \002whole number that is greater than zero (0)\002. \002\002 is the repeat count for the timer. • To perform the timer only once, use one (1) as the repeat count. • To repeat the timer until manually stopped, use zero (0) as the repeat count. • To repeat the timer X number of times, use that number as the repeat count. \002\002 is the command to perform when the timer fires. \002Example:\002 “msg #textual-testing Hey everyone!” \002Scope:\002 The channel or query that is selected when a timer is added is used as the target for commands that do not take a channel name as an argument. \002Example #1:\002 “/timer 10 1 ” — perform command one time, after ten seconds. \002Example #2:\002 “/timer 10 20 ” — perform command twenty times, once every ten seconds. \002Example #3:\002 “/timer 10 0 ” — perform command every ten seconds until the heat death of the universe."; "x1n-ve" = "“/timer list“ List timers"; "bx2-n1" = "“/timer stop “ Stop the timer assigned to \002\002. The identifier for a timer can be found using “/timer list”"; "r27-tv" = "“/timer restart “ Restart the timer assigned to \002\002. The identifier for a timer can be found using “/timer list” When restarting a timer, the iteration count is reset which means a timer that is setup to repeat X number of times will start over from the beginning."; "i2d-x5" = "“/timer remove “ Remove the timer assigned to \002\002. To remove all timers, pass “all” as \002\002. The identifier for a timer can be found using “/timer list”"; "aox-zz" = "---------------------------------------"; /* /setcolor/ command */ "026-qv" = "This command cannot be used unless the checkbox labeled “Disable nickname colors” is unchecked in the \002Style\002 section of \002Preferences\002."; "8dy-6f" = "Cannot set color for “%@“ because that does not appear to be a valid nickname."; /* /tage/ command */ "v9x-18" = "\002Time Since First Commit:\002 %@"; /* /topic/ command errors */ "1oo-3b" = "You have exceeded the maximum topic length for %1$@ which is %2$ld characters. The end of your topic may have been cut off."; /* /weights/ command */ "zud-u3" = "Nickname completion weights for %@:"; "24r-8c" = "\002%1$@\002: Sent: %2$.2f, Receive: %3$.2f, \002Total: %4$.2f\002"; "dje-41" = "No weights"; /* Address type migration warning */ "w05-ph" = "\0034\002Please take notice:\002\003 The preference labeled “Prefer IPv4”, which you had enabled, no longer exists. \002IPv6 has been disabled for this connection\002 to offer you similar behavior. See the \002Network Socket\002 section of \002Server Properties\002 to change which protocols are enabled (IPv4 and/or IPv6)."; /* Common command errors */ "atq-93" = "Invalid syntax: “%@“"; "pxa-ox" = "Cannot find channel named “%@“"; "1fc-vl" = "Cannot find query named “%@“"; "zef-q9" = "One or more arguments are not properly formatted for this command."; "g01-qn" = "This command can only be used with a channel."; "m6o-z1" = "This command can only be used with a query."; "sxf-qx" = "This command cannot be used within this window."; "z2r-sd" = "Messages cannot be sent to this window."; /* Miscellaneous messages related to IRC events */ "u51-nn" = "%1$@%2$@"; // RPL_ISUPPORT configuration data "xog-in" = "Be back later"; "bg3-h2" = "\002-CTCP-\002 %1$@ from %2$@ was ignored"; "jer-ju" = "CLIENTINFO DCC FINGER PING TIME USERINFO VERSION"; "en6-mw" = "Stop fingering me pervert!"; "5wa-lb" = "There are no capabilities that are enabled"; "7p9-rs" = "The following capabilities are currently enabled: %@"; "obp-ww" = "Mode is \002%@\002"; "7nm-7v" = "Topic is \002%@\002"; "y7s-3e" = "Set by \002%1$@\002 on %2$@"; "8tq-g6" = "Website is \002%@\002"; "zzj-2h" = "Attempting to rejoin channel in three seconds"; "qq2-66" = "\002%1$@\002 changed the topic to \002%2$@\002"; "4vt-ow" = "Connection to host established"; "l21-p7" = "Connection to host at [%@] established"; "ex4-f8" = "Connection secured using %@"; "drg-b7" = "Disconnected for Sleep Mode"; "wcl-po" = "Disconnected for server redirect"; "zro-bg" = "Disconnected from server because of an untrusted certificate"; "isx-fi" = "Disconnected from server because the Internet is not reachable"; "9b4-10" = "Disconnected"; "bps-la" = "%.0f minutes have elapsed since last response from this connection. Disconnecting due to timeout."; "gzo-54" = "%.0f minutes have elapsed since last response from this connection. It's possible the connection is timing out."; "3yo-gw" = "Message(%1$d): %2$@"; "o77-ls" = "Connecting to [%1$@] on port %2$hu"; "p7h-un" = "Connecting using SOCKS4 proxy [%1$@] on port %2$hu"; "ni5-cy" = "Connecting using SOCKS5 proxy [%1$@] on port %2$hu"; "oby-av" = "Connecting using HTTP proxy [%1$@] on port %2$hu"; "xxb-y2" = "Reconnecting…"; "ky3-36" = "Retrying…"; "v5d-ix" = "\002%1$@\002 sets mode \002%2$@\002"; "vy7-pk" = "\002-%1$@ CTCP-\002 %2$@ (%3$1.5f sec)"; "dri-l7" = "\002-%1$@ CTCP-\002 %2$@ %3$@"; "6o8-eu" = "\002-CTCP-\002 %1$@ from %2$@"; "tok-st" = "%1$@ (%2$@)"; "8hg-7k" = "%@ IRC Network"; "fxw-5s" = "%1$@ is now known as \002%2$@\002"; "rr6-yo" = "You're now known as \002%@\002"; "53b-dm" = "\002%1$@\002 (%2$@@%3$@) left IRC"; "8bk-mx" = "\002%1$@\002 left the query by disconnecting from IRC"; "q0q-ch" = "\002%1$@\002 joined the query by connecting to IRC"; "ipj-34" = "%@, your user modes are \002%@\002"; "wk4-rv" = "Inviting %1$@ to join %2$@"; "qw4-t3" = "%1$@!%2$@@%3$@ invited you to join %4$@"; "c1h-fq" = "%1$@ is away (%2$@)"; "6bh-br" = "You are now an IRC Operator on %@"; "ziu-p9" = "\002%1$@\002 (%2$@@%3$@) joined the channel"; "9aj-bd" = "\002%1$@\002 kicked \002%2$@\002 from the channel (%3$@)"; "nkr-kf" = "\002%1$@\002 (%2$@@%3$@) left the channel"; "ozy-6i" = "%1$@ (%2$@)"; "onk-l5" = "%1$@ is in %2$@"; "h19-n2" = "%1$@ is connected on %2$@ (%3$@)"; "plg-lr" = "%1$@ has userhost %2$@@%3$@ and real name “%4$@”"; "6hn-o6" = "%1$@ signed on at %2$@ and has been idle for %3$@"; "cdu-ed" = "%1$@ was connected to %2$@ on %3$@"; "32c-87" = "%1$@ had userhost %2$@@%3$@ and real name “%4$@”"; "11i-ev" = "You cannot send private messages to %@. Anything that you send to them will be ignored until you are added to their list of recognized users."; "3yj-in" = "%1$@ (%2$@) tried to send you a private message, but it was ignored due to \002umode +g\002. Perform “/accept %1$@” to allow this user to communicate with you."; "5i4-qq" = "Reconnecting to proxy to rebuild internal state…"; // irssi proxy "5h5-sl" = "Server closed read stream"; "r5h-fj" = "Joining channels has been delayed because because you haven't identified. Perform “/autojoin” to join channels without identifying."; "3oa-mv" = "%1$@ is connected from %2$@ (%3$@)"; "x69-rz" = "%1$@ was connected from %2$@ (%3$@)"; "js3-9v" = "Cannot use nickname “%@“ — Trying another…"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/Notifications.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Notification spoken text */ /* Address Book spoken text uses the actual notification message for that notification type. It is not defined here */ /* Channel messages */ /* Channel messages are broken up into multiple components so that they can be reassembled based on user preference. */ "tao-i2" = "Channel Message"; "kk4-68" = "Channel Notice"; "qld-sa" = "Highlight"; "ke8-17" = " in %@"; "6nw-ec" = " in Private Message"; "qy9-86" = " by %@"; "a7i-pu" = " from %@"; "g9t-bt" = ", "; // separator /* Other spoken text */ "l5i-at" = "Invited to %1$@ by %2$@"; "5yu-bf" = "Kicked from %1$@ by %2$@"; "rvb-9l" = "New Private Message from %1$@, %2$@"; "2bu-ep" = "Private Message from %1$@, %2$@"; "6jl-vh" = "Private Notice from %1$@, %2$@"; "z4p-yr" = "Connected to %@"; "fd0-f8" = "Disconnected from %@"; "5e4-vg" = "File transfer to %@ completed"; "4cd-p3" = "File transfer from %@ completed"; "f0u-32" = "File transfer to %@ failed"; "mak-bj" = "File transfer from %@ failed"; "at0-vi" = "Received file transfer request from %@"; "bwu-ps" = "%1$@ joined %2$@"; "vhl-5u" = "%1$@ joined"; // join without channel name (unused) "4aq-hz" = "%1$@ parted %2$@"; "nsn-du" = "%1$@ parted"; // part without channel name (unused) "sqf-4y" = "%1$@ disconnected"; // disconnect /* Notification bubble titles */ "niq-32" = "Address Book Notification"; "ep5-de" = "Channel Message: %@"; "chi-km" = "Channel Notice: %@"; "mo1-vn" = "Connected: %@"; "7xe-ig" = "Disconnected: %@"; "qka-f3" = "Highlight: %@"; "g4s-cq" = "Invited: %@"; "u30-ia" = "Kicked: %@"; "ltn-hf" = "New Private Message"; "69i-dy" = "Private Message"; "7hn-dg" = "Private Notice"; "l5y-sx" = "Successful File Transfer: %@"; "hc9-7n" = "Successful File Transfer: %@"; "het-vh" = "File Transfer Failed: %@"; "hm4-ze" = "File Transfer Failed: %@"; "nqz-7v" = "File Transfer Request: %@"; "keq-ts" = "User Joined: %@"; "im4-p0" = "User Parted: %@"; "20x-32" = "User Disconnected: %@"; /* Notification bubble descriptions. These are only notifications that require localization. Many notifications just send the raw message received so they are not localized here */ "88k-kl" = "Connection successful"; "bif-2c" = "Disconnection successful"; "xl5-dn" = "%1$@ invited you to join %2$@"; "fkt-p3" = "%1$@ kicked you from %2$@ with reason: %3$@"; "fhn-dd" = "%1$@ (%2$qi bytes)"; "oqh-pn" = "%1$@ (%2$qi bytes)"; "9r4-cq" = "%@"; "cqq-ci" = "%@"; "wik-wq" = "%1$@ (%2$qi bytes)"; "97r-0l" = "User with nickname “%1$@” is available"; "rif-9r" = "User with nickname “%1$@” is no longer available"; "xk2-1l" = "User with nickname “%1$@” is now available"; "yas-us" = "%1$@ joined %2$@"; "bu2-9m" = "%1$@ parted %2$@"; "3ur-i8" = "%1$@ parted %2$@ with reason: %3$@"; "7ao-n8" = "%1$@ disconnected"; "ssw-m6" = "%1$@ disconnected with reason: %2$@"; /* Notification bubble reply button placeholders */ "3t4-kl" = "Reply"; "bhn-uo" = "Send Message"; "do4-2e" = "Enter message to reply to this private message"; /* Notification type titles */ "kx3-xk" = "Address Book Notification"; "qnz-k4" = "Channel Message"; "vuq-jp" = "Channel Notice"; "4lr-ej" = "Connected"; "wjv-yb" = "Disconnected"; "cs4-x9" = "Highlight (Mention)"; "eiu-8q" = "Channel Invitation"; "2nk-lg" = "Kicked from Channel"; "5yi-gu" = "Private Message (new)"; "00b-nx" = "Private Message"; "nhz-io" = "Private Notice"; "0x2-3h" = "Successful File Transfer (Sending)"; "qle-7v" = "Successful File Transfer (Receiving)"; "sc0-1n" = "Failed File Transfer (Sending)"; "we9-1b" = "Failed File Transfer (Receiving)"; "st5-0n" = "File Transfer Request"; "25q-af" = "User Joined Channel"; "k3s-by" = "User Parted Channel"; "0fo-bt" = "User Disconnected"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/OffTheRecord.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * **********************************************************************\* */ "l49-9y" = "Off-the-Record Messaging: Private conversation started with an UNRECOGNIZED user. Perform authentication."; "r3w-fj" = "Off-the-Record Messaging: Private conversation started with a recognized user."; "rj0-ys" = "Off-the-Record Messaging: %@ was successful in confirming their identity."; "ufr-mh" = "Off-the-Record Messaging: %@ FAILED to confirm their identity."; "m1z-eb" = "Off-the-Record Messaging: Private conversation has ended."; "f59-3b" = "Off-the-Record Messaging: Refreshing the private conversation…"; "aii-2q" = "Off-the-Record Messaging: Starting a private conversation…"; "jww-e0" = "Off-the-Record Messaging: Our policy requires encryption but we are trying to send an unencrypted message. The message visible on your screen will be sent once a private conversation has started."; "jvt-fi" = "Off-the-Record Messaging: An error occurred while encrypting a message. The message was not sent."; "c5o-2l" = "Off-the-Record Messaging: Message has not been sent because the other user has closed the conversation."; "uhy-85" = "Off-the-Record Messaging: A private conversation could not be started."; "bl0-5i" = "Off-the-Record Messaging: Our original message has been sent back to us."; "c49-q0" = "Off-the-Record Messaging: The previous message was resent."; "6v9-w3" = "Off-the-Record Messaging: Received an encrypted message but cannot read it because a private conversation has not been started."; "9if-xp" = "Off-the-Record Messaging: Cannot read the message received."; "auo-n0" = "Off-the-Record Messaging: The message received contains malformed data."; "nl1-nf" = "Off-the-Record Messaging: Received a heartbeat"; "iwt-9f" = "Off-the-Record Messaging: Sent a heartbeat"; "2zx-p6" = "Off-the-Record Messaging: Received a generic error from the Off-the-Record Messaging library."; "1lf-f0" = "Off-the-Record Messaging: Received an unencrypted message."; "4by-8j" = "Off-the-Record Messaging: The message received was sent in a format that is not recognized."; "c7t-vi" = "Off-the-Record Messaging: Received and discarded a message intended for another instance."; "jrv-gq" = "Off-the-Record Messaging: Cannot authenticate user because a private conversation is not active."; "g9p-8r" = "Off-the-Record Messaging: Cannot end private conversation because there is not one that's active."; "8hr-8l" = "Requesting an off-the-record private conversation. However, you do not have a plugin to support that. See http://otr.cypherpunks.ca/ for more information."; "anu-ky" = "Not Encrypted"; "w34-mg" = "Encrypted & Unverified"; "l9n-p9" = "Encrypted & Verified"; "67n-6j" = "Off-the-Record Messaging: The fingerprint for %1$@ is “%2$@” — Perform authentication"; "mnw-qt" = "Are you sure you want to transfer the file named “%@“?"; "nuo-8s" = "This file will be sent without encryption."; "ng0-5q" = "Transfer File"; "gcg-tv" = "Are you sure you want to transfer the file named “%1$@“ to %2$@?"; "7he-76" = "This file will be sent without encryption."; "0v0-ix" = "Transfer File"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/Prompts.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "mvh-ms" = "Yes"; "99q-gg" = "No"; "zjw-bd" = "Continue"; "qso-2g" = "Cancel"; "c7s-dq" = "OK"; "xne-79" = "Select"; "ce6-o1" = "Choose"; "aqw-q1" = "Close"; "qpv-go" = "Accept"; "0sy-uc" = "Deny"; "u09-hg" = "Trust"; "tlp-8e" = "Delete"; "71s-kb" = "Open"; "ece-dd" = "Remind Me Later"; "68u-z9" = "Do not show this message again"; "6vj-2p" = "Are you sure you want to quit Textual?"; "77u-vp" = "Quitting will disconnect you from any server that you are connected to until you restart Textual."; "1bf-k0" = "Quit"; "7gr-e4" = "Set User vHost"; "2mx-jf" = "Please enter desired vHost (e.g. “apple.vhost”)"; "akr-eh" = "Enter some text to search for"; "d2w-4o" = "The keyboard shortcut ⌘G can be used to continue to the next search result. ⌘⇧G can be used to return to the previous search result."; "q5h-xx" = "Search"; "i8o-7z" = "Do you want to delete the selection?"; "516-ms" = "There is no undo and all data related to this channel, except for logs, will be erased."; "0kz-wd" = "Do you want to delete the selection?"; "etl-ss" = "There is no undo and all data related to this server, except for logs, will be erased."; "3l6-3z" = "You have clicked a link that will connect you to “%1$@“ and join the channel %2$@"; "mx1-qz" = "The connection named “%1$@“ is already configured to connect to this server address. Do you want to add this channel to that connection or would you rather create a new connection?"; "sl5-rf" = "Use Existing Connection"; "xca-5h" = "Create New Connection"; "pnc-ew" = "You have clicked a link that will connect you to “%1$@“ and join the channels: %2$@"; "a9z-9f" = "The connection named “%1$@“ is already configured to connect to this server address. Do you want to add these channels to that connection or would you rather create a new connection?"; "0hh-sl" = "Use Existing Connection"; "sv9-8s" = "Create New Connection"; "82q-zi" = "Are you sure you wish to enable inline media?"; "vcq-sz" = "Inline media does not use the proxy you may have configured for connecting to IRC. Your IP address will be leaked without configuring a system-wide proxy. If you wish to configure a proxy before continuing, click the “Open System Settings” button."; "xkj-nw" = "Turn On"; "x3e-ur" = "Open System Settings"; "9mb-o5" = "An update is available for these addons: %@"; "x4w-is" = "Would you like to open the “Textual Extras” installer to perform an update?"; "ioq-nf" = "Open Installer"; "ins-op" = "An update is required for these addons: %@"; "34o-pk" = "These addons will not be available for use until they are updated. Would you like to open the “Textual Extras” installer to perform an update?"; "hd0-bf" = "Remind Me Next Launch"; "467-5l" = "Open Installer"; "h78-9e" = "View Files"; "af6-45" = "The version of Textual that you are using is no longer compatible with these addons: %@"; "45a-df" = "Please contact the developer of each addon to receive information on updating. If you are the developer of one of these addons: In the Info.plist file of your project, set the value of the property “MinimumTextualVersion” to “%@“, then rebuild to check for any errors."; "324-5d" = "Remind Me Next Launch"; "0ik-o9" = "View Files"; "o9p-4n" = "The “Textual Extras” package must be installed to perform the command “%@”"; "bpb-vv" = "Would you like to open the installer?"; "6lr-02" = "Open Installer"; "2ul-cl" = "You have clicked on a link that will open the application “%@”"; "5oq-vv" = "Are you sure you want to open “%@” with this application?"; "hcb-3i" = "Preferences may become corrupted if you have more than one copy of Textual open"; "kx4-q8" = "Are you sure you want to continue?"; "py0-cr" = "The style named “%@” is not designed to work with this version of Textual"; "76t-pn" = "Some features will not work correctly with this style selected."; "2a3-5s" = "Choose Different Style"; "ezn-rm" = "The style named “%@” wants to enable dark mode but styles are no longer allowed to change that preference"; "193-6o" = "Do you want to keep Textual's light appearance or switch to dark mode?"; "hf0-w3" = "Keep It Light"; "hv0-79" = "Paint It Black"; "fjw-hj" = "The style named “%@” has been modified in such a way that it can no longer be used"; "3wd-gj" = "This usually occurs when one or more critical files have been moved or deleted."; "c4z-2b" = "Choose Different Style"; "1fm-up" = "This action will save a copy of your configuration to a location of your choice"; "syp-al" = "Please note that the following items cannot be exported: custom addons, file transfer download location, log location, and passwords."; "vun-f0" = "Save File"; "itb-3x" = "This action will overwrite your configuration with the contents of a file you choose"; "jsh-1a" = "Please note that the following items cannot be imported: custom addons, file transfer download location, log location, and passwords."; "502-6h" = "Choose File"; "6hx-ni" = "Save"; "0bj-ic" = "To install this script, save it inside the folder: [home folder] \U279C Library \U279C Application Scripts \U279C %@ Create this folder if it does not exist."; "m2r-gv" = "A script cannot be saved here"; "ztu-nv" = ""; "4ua-v5" = "The script named “%@” has been successfully installed"; "3ze-xh" = "Type “/%@” into the main input text field to perform this script."; "xek-0t" = "The extension named “%@” has been successfully installed"; "k69-q0" = "Restart Textual to load this extension."; "xfl-8e" = "Are you sure you want to open the file named “%@” with Textual?"; "6tj-yp" = ""; "k55-19" = "No logs were found"; "f05-hu" = ""; "b7o-v4" = "Textual is unable to access the folder that's configured for your log location because the resource is 'stale'"; "atn-1c" = "Navigate to Preferences \U279C Advanced \U279C Log Location to repair this preference"; "h99-3q" = "The process responsible for managing scrollback is not working correctly. Please report this problem to support@codeux.com"; "nlz-um" = "Last known error message: %@"; "d22-76" = "A query named “%@“ already exists. Do you want to delete this query?"; "61s-jc" = "There is no undo and all data related to this query, except for logs, will be erased."; "bi7-ah" = "Logging has been disabled because there is not enough space on your Mac"; "v9e-jy" = "Logging will resume when there is free space available."; "m8b-58" = "Textual can’t verify the identity of the server “%@”"; "85z-qw" = "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “%@”, which could put your confidential information at risk. Would you like to connect to the server anyway?"; "sfx-xx" = "Textual is using an encrypted connection to “%@“"; "ihy-mz" = "Encryption with a digital certificate keeps information private as it’s sent to or from the server “%1$@“"; "iun-45" = "Encryption with a digital certificate keeps information private as it’s sent to or from the server “%1$@“ Information encrypted using: %2$@"; "2jq-t5" = "%1$@ with the cipher suite: %2$@"; "8ou-pu" = "%1$@ with the cipher suite: %2$@ (deprecated)"; "ujw-64" = "The “Inspect Element” feature of WebKit2 cannot be accessed on this version of macOS."; "kig-m1" = "Disable WebKit2 or upgrade to a newer version of macOS."; "ios-na" = "Textual has copied the following content to a new location your Mac:"; "qy4-5o" = "Caches, custom addons, custom styles, encryption keys, preferences, and scrollback history. Would you like to clear up disk space by removing this content from the old location?"; "q3t-45" = "I would like to remove the old content"; "d90-au" = "Learn More"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCAboutDialog.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "zjd-al" = "Version %@"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCAddressBookSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "csu-bv" = "Please enter a properly formatted ignore mask."; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCChannelBanListSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "rhc-ke" = "Bans in %@"; "gbi-wn" = "Ban Exceptions in %@"; "ylc-6e" = "Invite Exceptions in %@"; "g4r-t6" = "Quiets in %@"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCChannelInviteSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "c8p-sb" = "%ld users"; "0lg-er" = "Invite %@ to:"; "7i1-ds" = "%1$@ and %2$@"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCChannelModifyModesSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "7m9-39" = "You have exceeded the secret key length defined by %1$@ which is %2$ld characters"; "lir-ra" = "If you continue typing, the end of your secret key may be cut off."; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCChannelModifyTopicSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "27l-qx" = "You have exceeded the maximum topic length defined by %1$@ which is %2$ld characters"; "zm4-cr" = "If you continue typing, the end of your topic may be cut off."; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCChannelPropertiesSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "zf2-r7" = "You have exceeded the secret key length defined by %1$@ which is %2$ld characters"; "op4-gg" = "If you continue typing, the end of your secret key may be cut off."; "1nd-7x" = "Please enter a properly formatted channel name."; "mvl-r5" = "This channel's configuration has changed. Do you want to reload the Channel Properties dialog?"; "qby-hi" = "You will loose unsaved changes if you click “Yes”"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCChannelSpotlightController.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "jpw-cj" = "%@"; "z68-5q" = " on %@"; "43s-x4" = "%@ unread message"; "vzj-30" = "%@ unread messages"; "0lz-oh" = "%@ highlight"; "c4u-21" = "%@ highlights"; "et7-c5" = "%1$@, %2$@"; "tyv-p6" = "No Results"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCFileTransferDialog.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "nvm-nd" = "%1$@ of %2$@ (%3$@/s) sent to %4$@"; "u17-ql" = "%1$@ of %2$@ (%3$@/s) sent to %4$@ — %5$@ remaining"; "w3h-p8" = "Transfer to %@ is stopped. Control click to start."; "j1z-88" = "Transfer to %@: Attempting to map listening port"; "onl-av" = "Transfer to %@: Determining local IP address"; "pcv-kg" = "Transfer to %@ is initializing"; "ca5-2v" = "Transfer to %@ is ready. Waiting for them to accept."; "rx7-xy" = "Transfer to %@ is complete"; "cku-24" = "Transfer to %@ is ready. Waiting for them to accept."; "7dk-lp" = "%1$@ of %2$@ (%3$@/s) received from %4$@"; "9xn-7j" = "%1$@ of %2$@ (%3$@/s) received from %4$@ — %5$@ remaining"; "jvh-u7" = "Transfer from %@ is stopped. Control click to start."; "495-90" = "Transfer from %@: Attempting to map listening port"; "6t1-mb" = "Transfer from %@: Determining local IP address"; "gxq-zu" = "Transfer from %@: Waiting for response to resume transfer"; "42z-mg" = "Transfer from %@ is initializing"; "pip-z6" = "Transfer from %@ is ready. Control click to start."; "6gu-za" = "Transfer from %@ is complete. Control click to open in Finder."; "7nf-fr" = "Connecting to %@"; "fn8-sx" = "Transfer with %@ failed. Could not establish connection"; "vxc-sd" = "Transfer with %@ failed. There is no open port"; "nab-dx" = "Transfer with %@ failed. Could not read source file"; "47s-1s" = "Transfer with %@ failed. Unknown source IP address"; "coa-ii" = "Transfer with %@ failed. Connection was disconnected"; "0ov-tr" = "Transfer with %@ failed. Proposed resume position is bad"; "12p-0v" = "Transfer with %@ failed. You are not connected to IRC"; "79f-s0" = "Transfer with %@ failed. No space left on device"; "05g-c8" = "Transfer with %@ failed. File handler threw an exception"; "s79-3a" = "Transfer with %@ failed: %@"; "dcm-w7" = "Select the folder in which this file will be saved"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCLicenseUpgradeEligibilitySheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "wmk-xg" = "A problem was encountered when checking the upgrade eligibility for the license key “%@”"; "8hb-y3" = "%@\n\nTry again in a few minutes then contact support if the problem persists."; "dn3-4r" = "Contact Support"; "awy-4i" = "Service returned a non-zero response code: %ld"; "z70-6s" = "Service returned an empty status context."; "gc5-ko" = "Service returned an malformed status context."; "5s6-sb" = "Service returned an unusual eligibility: %lu"; "9uu-go" = "Request cannot be completed at this time because it was rate limited."; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCPreferencesController.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "70s-c6" = "No Log Location Selected"; "721-ie" = "No Location Selected"; "5jv-aw" = "Are you opening this style because you are interested in modifying it?"; "ojj-ap" = "It’s possible to modify the appearance of this style without editing its source files. Click “Edit Style” to add your own style sheet rules. This feature can also be found in the Developers section of Preferences."; "aib-iy" = "Edit Style"; "dj8-1t" = "Create Copy"; "6ws-av" = "View Files"; "uc0-z7" = "Preferred Selection"; "q4o-2f" = "The style named “%1$@” has chosen to override the following preferences with ones that it prefers for the best viewing experience:\n\n%2$@"; "77t-de" = "• Nickname Format"; "ddh-hr" = "• Timestamp Format"; "we8-i8" = "• Channel View Font"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCPreferencesUserStyleSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "q4s-3m" = "/* !!! WARNING: THIS IS DESIGNED FOR ADVANCED USERS. !!! Styles are written in HTML which means custom rules should be written in CSS: https://developer.mozilla.org/en-US/docs/Web/CSS - Example: #topicBar { display: none; } Textual does not perform sanitation in any form on your rules which means you could possibly abuse this feature by escaping from the style element and inserting JavaScript or other code. */"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCServerChannelListDialog.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "7qf-r0" = "Channel List for “%@”"; "ct4-wh" = "Channel List — %@ Public Channels"; // %@ = channel count. ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCServerEndpointListSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "iis-gr" = "The value you entered is not a properly formatted server address"; "k0c-3u" = ""; "qeb-ip" = "The value you entered is not a properly formatted server port"; "ox2-od" = "Enter a whole number between 1 and 65,535."; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TDCServerPropertiesSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Server properties Address Book entry types */ "f7o-x4" = "User Ignore"; "b0g-0x" = "User Tracking"; /* Server specific highlight entry things */ "61f-6b" = "All Channels"; "qcc-b4" = "Exclude"; "tet-dk" = "Match"; /* Server properties navigation menu */ "lww-pc" = "Server Properties"; "v27-8w" = "Vendor Specific"; "8uw-tz" = "Advanced"; "8zc-6y" = "Address Book"; "5oz-07" = "Channel List"; "hip-13" = "Connect Commands"; "8ug-ka" = "Encoding"; "ehx-4d" = "General"; "8ik-qo" = "Identity"; "jtx-hn" = "Highlights"; "j34-yr" = "Messages"; "fsj-7f" = "ZNC Bouncer"; "ce7-kc" = "Client Certificate"; "fcr-w8" = "Flood Control"; "ffy-xt" = "Network Socket"; "t52-7a" = "Proxy Server"; "36n-u9" = "Redundancy"; /* Error localizations */ "8iw-q8" = "Please enter a properly formatted username."; "agy-bp" = "Please enter a properly formatted real name."; "tlo-b6" = "Please enter a properly formatted proxy address."; "wlz-tb" = "Please enter a list of properly formatted nicknames. Failed on nickname: “%@“ List of nicknames should be space separated. For example: “Guest1 Guest2 Guest3“"; /* Custom client-side certificate selection */ "6xz-ec" = "No Certificate Selected"; "6wq-i4" = "Choose An Identity"; "mi4-fd" = "Select a certificate to send when you connect securely."; "pmk-os" = "No Certificates Available"; "489-hG" = "There are no certificates in the keychain configured for identity purposes. Certificates must meet the following criteria: • Certificate is configured as type: “SSL Client” • Certificate has matching private key"; "3ju-lo" = "Learn to Create a Certificate"; /* Cipher suites */ "yko-5g" = "The “%@” includes the following cipher suites:"; "k50-8n" = "%@\n\nThese cipher suites are ordered by preference with the most preferred at the top."; /* Enabling wait for NickServ without a password */ "94r-eq" = "The preference you have enabled will not work correctly because you do not have a password set"; "26u-j8" = "Enter your NickServ password into the field labeled “Personal Password”"; /* Cloud changes */ "bzh-il" = "This connection's configuration has changed. Do you want to reload the Server Properties dialog?"; "oz4-kb" = "You will loose unsaved changes if you click “Yes”"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TLOLicenseManager.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "nhh-ts" = "A problem was encountered while communicating with the web server located at codeux.com"; "1cv-0v" = "Try again in a few minutes then contact support if the problem persists."; "bqw-cv" = "Contact Support"; "13u-4p" = "codeux.com has denied your request because too many requests have been received in a short period of time"; "fvx-90" = "Access to license information for Textual is rate limited to prevent misuse or abuse. Please try again in a few minutes."; "u8h-qv" = "The license key “%@“ cannot be activated because it is blacklisted"; "w1n-n0" = "Contact support for more information."; "vgp-j6" = "Contact Support"; "ocm-03" = "The following error was returned while processing request:"; "dio-y9" = "Improperly formatted e-mail address or missing"; "fg6-gf" = "The following error was returned while processing request:"; "wc7-mn" = "Unknown or improperly formatted license key"; "m4q-ul" = "Successfully queued e-mail for %@"; "fxj-s6" = "An e-mail which contains your license key will be delivered to %@ within the next few minutes."; "jbs-64" = "Thank you for your purchase of Textual!"; "k39-7l" = "All limitations have been lifted."; "pg1-a9" = "Are you sure you want to deactivate this copy of Textual?"; "z87-wb" = "All limitations that apply to the trial version of Textual will immediately take effect once deactivated."; "vft-qo" = "Select a copy of Textual downloaded from the Mac App Store to receive a license key. You can download a new copy at any time in the Purchases section of your Mac App Store account."; "b6l-ka" = "The application “%@“ cannot be used to request a license key"; "sdj-xd" = "This application is not Textual 5 or a newer version, or was not downloaded from the Mac App Store."; "vxq-oa" = "Successfully migrated Mac App Store purchase"; "yxk-ej" = "An e-mail which contains your license key will be delivered to %@ within the next few minutes."; "enb-hw" = "Cannot migrate Mac App Store purchase because it has already been migrated"; "36y-49" = "Click the “Recover Lost License” button if you have lost your license key."; "ztd-5y" = "The following error was returned while processing request:"; "bu4-zk" = "Improperly formatted e-mail address or is missing"; "r87-jw" = "The following error was returned while processing request:"; "6zh-jr" = "Unable to locate an order that is linked to the e-mail address “%@“"; "o66-ox" = "The license key “%@“ has exceeded its daily activation limit"; "6aa-ow" = "This license key cannot be activated more than %ld times within a twenty-four (24) hour period. Please try again at a later time."; "wdl-3f" = "Your trial of Textual will expire in %@"; "kn7-ju" = "Your trial of Textual has expired."; "ccj-ag" = "Please purchase Textual to prevent disruption to your chat experience."; "b8b-sg" = "More Info"; "f49-rk" = "The following error was returned while processing request:"; "do9-8x" = "Improperly formatted name or is missing"; "p9s-ak" = "The following error was returned while processing request:"; "ujo-cd" = "Receipt did not pass validation because of the following error:\n\n“%@“"; "0uc-io" = "The license key “%@“ cannot be activated because another operation is already in progress"; "vnu-5e" = ""; "bw3-sc" = "The license key “%@“ cannot be activated because it is already in use"; "fxu-su" = ""; "pmv-qp" = "The license key “%@“ cannot be activated because it requires an upgrade"; "5bu-ov" = ""; "53u-lt" = "Learn More"; "t28-j9" = "Cannot migrate Mac App Store purchase because the appropriate in-app purchase is missing"; "4n2-ps" = "The Standard Edition in-app purchase is required to perform migration."; "12h-3w" = "Are you sure you want to purchase a new license key?"; "dpo-ln" = "You already own the license key “%@.”"; "qrx-aq" = "Upgrade Existing License Key"; "zp8-8q" = "Purchase New License Key"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TVCMainWindow.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "tjj-z2" = "No Privileges"; "ya1-sk" = "Voice"; "0nn-te" = "Half-op"; "0kn-s5" = "Operator"; "som-zo" = "Super Admin"; "p1z-sc" = "Channel Owner"; "i8t-vb" = "Server Staff Member (IRC Operator)"; "jkr-ed" = "User is away"; "gi6-wf" = "User is not away"; "d85-9n" = "Information Not Available"; "8r3-ih" = "Send message…"; "jqk-ha" = " — "; "cyg-g9" = " (%@)"; "v6i-zb" = " (%@ users)"; "19v-bc" = "%1$@%2$@ on %3$@"; "q42-an" = "Disconnected — "; "xqd-h9" = "Disconnecting… — "; "8eu-c7" = "Connecting… — "; "wcb-y8" = "Logging on… — "; "s23-zd" = "Reconnecting… — "; "3yn-wd" = "Waiting to reconnect… — "; "nxz-l9" = " (away)"; "6wz-pd" = "%1$@ (%2$@)"; "iph-a9" = "Loading configuration…"; "5g1-i9" = "Importing preferences…"; "4yo-mk" = "Current Session"; "vi3-23" = "(No Topic)"; ================================================ FILE: Sources/App/Resources/Language Files/en.lproj/TVCNotificationConfigurationView.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "vje-9n" = "No Sound"; "0rs-8l" = "Default Sound"; ================================================ FILE: Sources/App/Resources/License Manager/RemoteLicenseSystemPublicKey.pub ================================================ -----BEGIN RSA PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7ZYeINFUwe5zOpGqIKUD ohdMprjucrmhnCMJndVTJLlHDdttKdfXF8ar39EGkh/WeEDL6i/tLaMYzXEkRWzJ a/ZKY0j6L4MFfAn3Div+HtAAKKp4Ag4dCJALsfaldhz0wapEttifs+H7cDqX1sbC lQQzFJNOufdLf1bTJqXnnnt/dB6D7xK1Poe/T1meTjWbJISpujm6bib3n8GyOTbB dQMBd8RiGD6t9Scm6feoESVAVJviCMS+/IHWtVp0EenStqzFgy/zl4R9Op5ypoYY Uo8Ul6Q/pS/OpAH/plJ/8duAbvuTy5fWo7MNbgHV7DfYLk2FrXKgyUgjEjOfyfpF oSpuWbV52nPeYf3lgyIgqVif6eF/LEoIqMk1nDUrspWAkg2FHqTgik5nKo1fuezq LT2BkdV+nSee7ZjmWcSQD3Lb/D826Zb5EJcpvlK4dw2QX4VtQ4dHeGRmnA04/IDd qAAJ0fwzVGItehwuhbtPYFFdEHbMpqqTRTKu9BvRfc3zd79b2VXIIjQykPVafc6o AnUiXZStO9vvstXjwnG4pKzCKdy3IRKgn4gNp91wHwOlY6mgRoEEHb5q1jtj0ZiR Lr7oKr4UK6a3/KxmVRKcIIhHQZ16iKWTwOp3kx/OX9jNMF13tr4fgUAAuuPnowIB CBA1ua7CrIfxIbUM7PscG2sCAwEAAQ== -----END RSA PUBLIC KEY----- ================================================ FILE: Sources/App/Resources/Property Lists/Application Properties/Info.plist ================================================ LSApplicationCategoryType public.app-category.social-networking LSRequiresNativeExecution CFBundleSignature TXTL CFBundleDisplayName Textual IRC Client CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleName ${PRODUCT_NAME} CFBundlePackageType APPL CFBundleVersion 7.2.6 CFBundleShortVersionString 7.2.6 NSHumanReadableCopyright Copyright © 2010 - 2023 Codeux Software, LLC. All rights reserved. CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIconFile applicationIcon.icns CFBundleInfoDictionaryVersion 6.0 NSMainNibFile TXCMainMenu LSMinimumSystemVersion 11.0 NSPrincipalClass TXApplication NSContactsUsageDescription Textual accesses contacts to automatically populate certain forms with your name and/or e-mail address. Access to this information is not required. NSAppleScriptEnabled NSAppTransportSecurity NSAllowsArbitraryLoads CFBundleDocumentTypes CFBundleTypeExtensions scpt CFBundleTypeName Textual IRC Client Script CFBundleTypeOSTypes **** CFBundleTypeRole Viewer LSHandlerRank Alternate LSTypeIsPackage 0 NSDocumentClass TPCResourceManagerDocumentTypeImporter CFBundleTypeExtensions bundle CFBundleTypeName Textual IRC Client Extension CFBundleTypeRole Editor LSHandlerRank Alternate LSTypeIsPackage 1 NSDocumentClass TPCResourceManagerDocumentTypeImporter CFBundleURLTypes CFBundleTypeRole Viewer CFBundleURLIconFile application CFBundleURLName Internet Relay Chat CFBundleURLSchemes irc ircs textual SUAllowsAutomaticUpdates SUBundleName Textual SUEnableDownloaderService SUEnableInstallerLauncherService SUEnableSystemProfiling SUFeedURL https://updates.textualapp.com/sparkle/feeds/v7/feed-one-merged.xml SUScheduledCheckInterval 345600 ================================================ FILE: Sources/App/Resources/Property Lists/IRCCommandIndexLocalData.plist ================================================ Reserved Information Next Index Value 5119 adchat developerModeOnly indexValue 5001 arguments <message> ame developerModeOnly indexValue 5002 arguments <message> amsg developerModeOnly indexValue 5003 arguments <message> aquote developerModeOnly indexValue 5095 arguments <input> araw developerModeOnly indexValue 5096 arguments <input> autojoin developerModeOnly indexValue 5101 away developerModeOnly indexValue 5004 arguments [comment] back developerModeOnly indexValue 5105 ban developerModeOnly indexValue 5005 arguments [channel] <nickname> cap developerModeOnly indexValue 5006 caps developerModeOnly indexValue 5007 chatops developerModeOnly indexValue 5009 clear developerModeOnly indexValue 5010 clearall developerModeOnly indexValue 5011 close developerModeOnly indexValue 5012 arguments [target] conn developerModeOnly indexValue 5013 arguments [server] ctcp developerModeOnly indexValue 5014 arguments <nickname> <command> [extra] ctcpreply developerModeOnly indexValue 5015 arguments <nickname> <command> [extra] cycle developerModeOnly indexValue 5016 dcc developerModeOnly indexValue 5017 debug developerModeOnly indexValue 5018 arguments <message> defaults developerModeOnly indexValue 5092 New item <enable | disable> [-a] <feature> dehalfop developerModeOnly indexValue 5019 arguments [channel] <nickname> deop developerModeOnly indexValue 5020 arguments [channel] <nickname> devoice developerModeOnly indexValue 5021 arguments [channel] <nickname> echo developerModeOnly indexValue 5022 arguments <message> getscripts developerModeOnly indexValue 5098 gline developerModeOnly indexValue 5023 arguments <+ | -><hostmask | nickname> [duration] [comment] globops developerModeOnly indexValue 5024 arguments <message> goto developerModeOnly indexValue 5099 arguments <needle> gzline developerModeOnly indexValue 5025 arguments <+ | -><hostmask | nickname> [duration] [comment] halfop developerModeOnly indexValue 5026 arguments [channel] <nickname> hop developerModeOnly indexValue 5027 ignore developerModeOnly indexValue 5029 arguments <nickname> invite developerModeOnly indexValue 5030 arguments <nickname> [channel] ison developerModeOnly indexValue 5100 arguments <nickname> j developerModeOnly indexValue 5031 arguments <channel[,channel]]> [key[,key]] join developerModeOnly indexValue 5032 arguments <channel[,channel]]> [key[,key]] join_random developerModeOnly indexValue 5109 arguments [count] kb developerModeOnly indexValue 5083 arguments [channel] <nickname> [comment] kick developerModeOnly indexValue 5033 arguments [channel] <nickname> [comment] kickban developerModeOnly indexValue 5034 arguments [channel] <nickname> [comment] kill developerModeOnly indexValue 5035 arguments <nickname> [comment] lagcheck developerModeOnly indexValue 5084 leave developerModeOnly indexValue 5036 arguments [comment] list developerModeOnly indexValue 5037 locops developerModeOnly indexValue 5039 arguments <message> m developerModeOnly indexValue 5040 arguments <target> [flags] [arguments] me developerModeOnly indexValue 5041 arguments <message> mode developerModeOnly indexValue 5042 arguments <target> [flags] [arguments] monitor developerModeOnly indexValue 5106 msg developerModeOnly indexValue 5043 arguments <target[,target]]> <message> mute developerModeOnly indexValue 5044 mylag developerModeOnly indexValue 5045 myversion developerModeOnly indexValue 5046 nachat developerModeOnly indexValue 5047 arguments <message> names developerModeOnly indexValue 5094 arguments <channel> nick developerModeOnly indexValue 5048 arguments <nickname> notice developerModeOnly indexValue 5050 arguments <target[,target]]> <message> notifybubble developerModeOnly indexValue 5112 arguments [target] <message> notifysound developerModeOnly indexValue 5113 arguments <sound> notifyspeak developerModeOnly indexValue 5114 arguments <message> omsg developerModeOnly indexValue 5051 arguments <channel> <message> onotice developerModeOnly indexValue 5052 arguments <channel> <message> op developerModeOnly indexValue 5053 arguments [channel] <nickname> part developerModeOnly indexValue 5054 arguments [comment] pass developerModeOnly indexValue 5055 arguments <password> query developerModeOnly indexValue 5056 arguments <nickname> quit developerModeOnly indexValue 5057 arguments [comment] quiet developerModeOnly indexValue 5107 arguments [channel] <nickname> quote developerModeOnly indexValue 5058 arguments <input> raw developerModeOnly indexValue 5059 arguments <input> recv developerModeOnly indexValue 5087 arguments <input> rejoin developerModeOnly indexValue 5060 remove developerModeOnly indexValue 5061 arguments [target] server developerModeOnly indexValue 5062 arguments <address> [[+] port] [password] setcolor developerModeOnly indexValue 5103 arguments <nickname> setqueryname developerModeOnly indexValue 5117 arguments <nickname> shun developerModeOnly indexValue 5063 arguments <+ | -><hostmask | nickname> [duration] [comment] sme developerModeOnly indexValue 5064 arguments <target[,target]]> <message> smsg developerModeOnly indexValue 5065 arguments <target[,target]]> <message> sslcontext developerModeOnly indexValue 5066 t developerModeOnly indexValue 5067 arguments <channel> [topic] tage developerModeOnly indexValue 5093 tempshun developerModeOnly indexValue 5068 arguments <+ | -><hostmask | nickname> [comment] timer developerModeOnly indexValue 5069 arguments <seconds> <repeat> <command> topic developerModeOnly indexValue 5070 arguments <channel> [topic] ume developerModeOnly indexValue 5089 arguments <target> <message> umode developerModeOnly indexValue 5071 arguments [flags] [arguments] umsg developerModeOnly indexValue 5088 arguments <target> <message> unban developerModeOnly indexValue 5072 arguments [channel] <nickname> unignore developerModeOnly indexValue 5073 arguments <nickname> unmute developerModeOnly indexValue 5075 unotice developerModeOnly indexValue 5090 arguments <target> <message> unquiet developerModeOnly indexValue 5108 arguments [channel] <nickname> voice developerModeOnly indexValue 5076 arguments [channel] <nickname> wallops developerModeOnly indexValue 5077 arguments <message> watch developerModeOnly indexValue 5097 weights developerModeOnly indexValue 5118 who developerModeOnly indexValue 5079 arguments <channel> whois developerModeOnly indexValue 5080 arguments <nickname> whowas developerModeOnly indexValue 5081 arguments <nickname> zline developerModeOnly indexValue 5082 arguments <+ | -><hostmask | nickname> [duration] [comment] ================================================ FILE: Sources/App/Resources/Property Lists/IRCCommandIndexRemoteData.plist ================================================ Reserved Information Next Index Value 1058 adchat indexValue 1003 outgoingColonIndex 0 away indexValue 1050 outgoingColonIndex 0 authenticate indexValue 1005 outgoingColonIndex 999 batch indexValue 1054 outgoingColonIndex 999 cap indexValue 1004 outgoingColonIndex 999 certinfo indexValue 1055 outgoingColonIndex 999 chatops indexValue 1006 outgoingColonIndex 0 chghost indexValue 1057 outgoingColonIndex 999 error indexValue 1016 outgoingColonIndex 0 gline indexValue 1047 outgoingColonIndex 2 globops indexValue 1017 outgoingColonIndex 0 gzline indexValue 1048 outgoingColonIndex 2 invite indexValue 1018 outgoingColonIndex 999 ison indexValue 1019 outgoingColonIndex 999 join indexValue 1020 outgoingColonIndex 999 kick indexValue 1021 outgoingColonIndex 2 kill indexValue 1022 outgoingColonIndex 1 list indexValue 1023 outgoingColonIndex 999 locops indexValue 1024 outgoingColonIndex 0 mode indexValue 1026 outgoingColonIndex 999 monitor indexValue 1056 outgoingColonIndex 999 nachat indexValue 1027 outgoingColonIndex 0 names indexValue 1028 outgoingColonIndex 999 nick indexValue 1029 outgoingColonIndex 999 notice indexValue 1030 outgoingColonIndex 1 part indexValue 1031 outgoingColonIndex 1 pass indexValue 1032 outgoingColonIndex -1 ping indexValue 1033 outgoingColonIndex 999 pong indexValue 1034 outgoingColonIndex 999 privmsg indexValue 1035 outgoingColonIndex 1 quit indexValue 1036 outgoingColonIndex 0 shun indexValue 1045 outgoingColonIndex 2 tempshun indexValue 1046 outgoingColonIndex 1 topic indexValue 1039 outgoingColonIndex 1 user indexValue 1037 outgoingColonIndex 3 wallops indexValue 1038 outgoingColonIndex 0 watch indexValue 1053 outgoingColonIndex 999 who indexValue 1040 outgoingColonIndex 999 whois indexValue 1042 outgoingColonIndex 999 whowas indexValue 1041 outgoingColonIndex 999 zline indexValue 1049 outgoingColonIndex 2 ================================================ FILE: Sources/App/Resources/Property Lists/IRCNetworks.plist ================================================ AccessIRC serverAddress irc.accessirc.net serverPort 6697 prefersSecuredConnection AlphaChat serverAddress irc.alphachat.net serverPort 6697 prefersSecuredConnection Azzurra serverAddress irc.azzurra.chat serverPort 6697 prefersSecuredConnection DALnet serverAddress irc.dal.net serverPort 6667 prefersSecuredConnection EFnet serverAddress irc.efnet.info serverPort 6667 prefersSecuredConnection EsperNet serverAddress irc.esper.net serverPort 6697 prefersSecuredConnection Ewnix serverAddress irc.ewnix.net serverPort 6697 prefersSecuredConnection Fuel Rats serverAddress irc.fuelrats.com serverPort 6697 prefersSecuredConnection GameSurge serverAddress irc.gamesurge.net serverPort 6667 prefersSecuredConnection GeeksIRC serverAddress irc.geeksirc.net serverPort 6697 prefersSecuredConnection GeekShed serverAddress irc.geekshed.net serverPort 6697 prefersSecuredConnection German-Elite serverAddress irc.german-elite.net serverPort 6697 prefersSecuredConnection Global Gamers serverAddress irc.globalgamers.net serverPort 6697 prefersSecuredConnection IRCHighWay serverAddress irc.irchighway.net serverPort 6697 prefersSecuredConnection Interlinked serverAddress irc.interlinked.me serverPort 6697 prefersSecuredConnection IdleChat serverAddress irc.idlechat.net serverPort 6697 prefersSecuredConnection Libera Chat serverAddress irc.libera.chat serverPort 6697 prefersSecuredConnection LizardIRC serverAddress irc.lizardirc.org serverPort 6697 prefersSecuredConnection Mibbit serverAddress irc.mibbit.com serverPort 6697 prefersSecuredConnection MindForge serverAddress irc.mindforge.org serverPort 6697 prefersSecuredConnection Mozor serverAddress irc.mozor.net serverPort 6697 prefersSecuredConnection NixtrixIRC serverAddress irc.nixtrixirc.net serverPort 6697 prefersSecuredConnection OFTC serverAddress irc.oftc.net serverPort 6697 prefersSecuredConnection PonyChat serverAddress irc.ponychat.net serverPort 6697 prefersSecuredConnection QuakeNet serverAddress irc.quakenet.org serverPort 6667 prefersSecuredConnection Rizon serverAddress irc.rizon.net serverPort 6697 prefersSecuredConnection Snoonet serverAddress irc.snoonet.org serverPort 6697 prefersSecuredConnection Snyde serverAddress irc.snyde.net serverPort 6697 prefersSecuredConnection StormBit serverAddress irc.stormbit.net serverPort 6697 prefersSecuredConnection TWiT serverAddress irc.twit.tv serverPort 6697 prefersSecuredConnection Thinkstack serverAddress irc.thinstack.net serverPort 6697 prefersSecuredConnection TinyCrab serverAddress irc.tinycrab.net serverPort 6697 prefersSecuredConnection Undernet serverAddress irc.undernet.org serverPort 6667 prefersSecuredConnection freenode serverAddress chat.freenode.net serverPort 6697 prefersSecuredConnection hackint serverAddress irc.hackint.org serverPort 6697 prefersSecuredConnection ================================================ FILE: Sources/App/Resources/Property Lists/Preferences/KeysExcludedFromContainer.plist ================================================ com.adiumX.AutoHyperlinks.permittedSchemesAny 0 com.adiumX.AutoHyperlinks.permittedSchemesDefault 0 com.adiumX.AutoHyperlinks.permittedSchemes 0 NSWindow Frame -> Internal (v3) -> 1 SUEnableAutomaticChecks 0 SUHasLaunchedBefore 0 SULastCheckTime 0 SUScheduledCheckInterval 0 SUSendProfileInfo 0 SUUpdateGroupIdentifier 0 TLOLicenseManagerAuthorizationCode 0 ================================================ FILE: Sources/App/Resources/Property Lists/Preferences/KeysExcludedFromExport.plist ================================================ TXRunCount 0 TXRunTime 0 TextField 1 Window -> Main Window 1 THOPluginManager -> Extras Installer Last Check for Update Payload 0 Text Input Prompt Suppression -> 1 Textual 7 Upgrade -> 1 TPCPreferences -> Migration -> 1 NSWindow Frame -> Internal (v3) -> 1 SUEnableAutomaticChecks 0 SUHasLaunchedBefore 0 SULastCheckTime 0 SUScheduledCheckInterval 0 SUSendProfileInfo 0 SUUpdateGroupIdentifier 0 Internal Theme Settings Key-value Store -> 0 Theme -> Font Name -> Did Not Exist During Last Sync 0 Theme -> Name -> Did Not Exist During Last Sync 0 Theme -> Channel Font Preference Enabled 0 Theme -> Nickname Format Preference Enabled 0 Theme -> Timestamp Format Preference Enabled 0 File Transfers -> File Transfer Download Folder Bookmark 0 LogTranscriptDestinationSecurityBookmark_5 0 TLOLicenseManagerAuthorizationCode 0 TVCLogControllerHistoricLogFileSavePath_v3 0 Sandbox Migration -> Migrated Resources 0 Sandbox Migration -> Installation Migrated 0 Sandbox Migration -> User Acknowledged 0 Sandbox Migration -> User Prefers Pruning Files 0 Sandbox Migration -> All Extensions Pruned 0 Sandbox Migration -> Imported Keys 0 ================================================ FILE: Sources/App/Resources/Property Lists/Preferences/KeysExcludedFromMigrate.plist ================================================ NSWindowAutosaveFrameMovesToActiveDisplay 0 WebKitDeveloperExtras 0 __uniquePageGroupID-1.WebKit2ScrollAnimatorEnabled 0 __uniquePageGroupID-1.WebKit2ShouldRespectImageOrientation 0 TPCResourceManagerMigrate -> Migrated Resources 0 TPCResourceManagerMigrate -> Installation Migrated 0 TPCResourceManagerMigrate -> User Acknowledged 0 TPCResourceManagerMigrate -> User Prefers Pruning Files 0 TPCResourceManagerMigrate -> All Extensions Pruned 0 TPCResourceManagerMigrate -> Imported Keys 0 ================================================ FILE: Sources/App/Resources/Property Lists/Preferences/PreferenceKeyMasterList.plist ================================================ Appearance 0 ApplicationCTCPVersionMasquerade 0 ApplyCommandToAllConnections -> amsg 0 ApplyCommandToAllConnections -> away 0 ApplyCommandToAllConnections -> clearall 0 ApplyCommandToAllConnections -> nick 0 AutojoinChannelOnInvite 0 AutojoinDelayBetweenChannelJoins 0 AutojoinDelayAfterIdentification 0 AutojoinMaximumChannelJoinCount 0 AutomaticallyAddScrollbackMarker 0 AutomaticallyDisconnectForSleepMode 0 AutomaticallyDetectHighlightSpam 0 AutomaticallyFilterUnicodeTextSpam 0 AutomaticallyReloadCustomThemesWhenTheyChange 0 Blowfish Encryption Extension -> Enable Service 0 ChannelNavigationIsServerSpecific 0 ChannelOperatorDefaultLocalization -> Kick Reason 0 ChannelViewArrangement 0 CommandReturnSendsMessageAsAction 0 ConfirmApplicationQuit 0 ControlEnterSendsMessage 0 ConversationTrackingIncludesUserModeSymbol 0 CopyTextSelectionOnMouseUp 0 com.adiumX.AutoHyperlinks.permittedSchemesAny 0 com.adiumX.AutoHyperlinks.permittedSchemesDefault 0 com.adiumX.AutoHyperlinks.permittedSchemes 0 DefaultBanCommandHostmaskFormat 0 DefaultIdentity -> AwayNickname 0 DefaultIdentity -> Nickname 0 DefaultIdentity -> Realname 0 DefaultIdentity -> Username 0 DestinationOfNonserverNotices 0 DisableMainWindowSegmentedController 0 DisableRemoteNicknameColorHashing 0 DisableSidebarTranslucency 0 DisplayDockBadges 0 DisplayEventInLogView -> Date Changes 0 DisplayEventInLogView -> Inline Media 0 DisplayEventInLogView -> Join, Part, Quit 0 DisplayPublicMessageCountInDockBadge 0 DisplayServerMessageOfTheDayOnConnect 0 DisplayUserListNoModeSymbol 0 File Transfers -> File Transfer Download Folder Bookmark 0 File Transfers -> File Transfer IP Address Detection Method 0 File Transfers -> File Transfer IP Address Detection Prefers 3rd-party Sources 0 File Transfers -> File Transfer Port Range End 0 File Transfers -> File Transfer Port Range Start 0 File Transfers -> File Transfer Request Reply Action 0 File Transfers -> File Transfer Requests Use Reverse DCC 0 File Transfers -> Idle System Sleep Prevented During File Transfer 0 FocusSelectionOnMessageCommandExecution 0 Highlight List -> Excluded Matches 0 Highlight List -> Primary Matches 0 IRCopDefaultLocalizaiton -> G:Line Reason 0 IRCopDefaultLocalizaiton -> Kill Reason 0 IRCopDefaultLocalizaiton -> Shun Reason 0 InlineMediaCheckEverything 0 InlineMediaLimitToBasics 0 InlineMediaLimitBasicsToFiles 0 InlineMediaLimitInsecureContent 0 InlineMediaLimitNaughtyContent 0 InlineMediaLimitUnsafeContent 0 InlineMediaMaximumFilesize 0 InlineMediaMaximumHeight 0 InlineMediaScalingWidth 0 Internal Theme Settings Key-value Store -> 1 InvertSidebarColors 0 IRC -> Enable echo-message Capability 0 Keyboard -> Command+W Key Action 0 Keyboard -> Tab Key Action 0 Keyboard -> Tab Key Completion Suffix 0 LogHighlights 0 LogTranscript 0 LogTranscriptDestinationSecurityBookmark_5 0 MemberListSortFavorsServerStaff 0 MemberListUpdatesUserInfoPopoverOnScroll 0 Main Input Text Field -> Font Size 0 Main Input Text Field -> Focus When Changing Views 0 MainWindowTransparencyLevel 0 NicknameHighlightMatchingType 0 Nickname Color Style Overrides (v2) 0 New item 0 NotificationType -> 1 Notification Sound Is Muted 0 NSWindow Frame -> Internal (v3) -> 1 PostNotificationsWhileInFocus 0 PreferModernCiphers 0 PreferModernSockets 0 Private Extension Store -> Caffeine Extension -> Prevent Sleep 0 Private Extension Store -> Blowfish Encryption Extension -> Encryption Mode of Operation -> 1 Off-the-Record Messaging -> Automatically Enable Service 0 Off-the-Record Messaging -> Enable Encryption 0 Off-the-Record Messaging -> Require Encryption 0 OnlySpeakNotificationsForSelection 0 OpenClickedLinksInBackgroundBrowser 0 Optimizations -> Load History Lazily 0 ReceiveBetaUpdates 0 RejoinChannelOnLocalKick 0 ReloadScrollbackOnLaunch 0 RightToLeftTextFormatting 0 RemoveIRCTextFormatting 0 ReplyUnignoredExternalCTCPRequests 0 SaveInputHistoryPerSelection 0 ServerListDoubleClickConnectServer 0 ServerListDoubleClickDisconnectServer 0 ServerListDoubleClickJoinChannel 0 ServerListDoubleClickLeaveChannel 0 ServerListRetainsQueriesBetweenRestarts 0 Server List Unread Message Count Badge Colors -> Highlight 0 Server Properties Window Sheet -> Include Advanced Encodings 0 SetAwayOnScreenSleep 0 ScrollbackMaximumSavedLineCount 0 ScrollbackMaximumVisibleLineCount 0 SwipeMinimumLength 0 Smiley Converter Extension -> Enable Service 0 Smiley Converter Extension -> Enable Extra Emoticons 0 SUEnableAutomaticChecks 0 SUHasLaunchedBefore 0 SULastCheckTime 0 SUScheduledCheckInterval 0 SUSendProfileInfo 0 SUUpdateGroupIdentifier 0 System Profiler Extension -> Feature Disabled -> CPU Model 0 System Profiler Extension -> Feature Disabled -> GPU Model 0 System Profiler Extension -> Feature Disabled -> Disk Information 0 System Profiler Extension -> Feature Disabled -> Memory Information 0 System Profiler Extension -> Feature Disabled -> OS Version 0 System Profiler Extension -> Feature Disabled -> Screen Resolution 0 System Profiler Extension -> Feature Disabled -> System Uptime 0 Tab Completion -> Completion Suffix Cut Forward Until Space 0 Tab Completion -> Do Not Use Whitespace for Missing Completion Suffix 0 Text Input Prompt Suppression -> 1 TextFieldAutomaticGrammarCheck 0 TextFieldAutomaticSpellCheck 0 TextFieldAutomaticSpellCorrection 0 TextFieldSmartCopyPaste 0 TextFieldSmartDashes 0 TextFieldDataDetectors 0 TextFieldSmartLinks 0 TextFieldSmartQuotes 0 TextFieldTextReplacement 0 TextualDeveloperEnvironment 0 Textual 7 Upgrade -> Tv7 -> Remind Me Later 0 Textual 7 Upgrade -> Tv7 -> Last Dialog Presentation (LMD) 0 Textual 7 Upgrade -> Tv7 -> Eligible License Key 0 Textual 7 Upgrade -> Tv7 -> Eligibility 0 Textual Chat Filter Extension -> Filters 0 Theme -> Channel Font Preference Enabled 0 Theme -> Font Name 0 Theme -> Font Name -> Did Not Exist During Last Sync 0 Theme -> Font Size 0 Theme -> Name 0 Theme -> Name -> Did Not Exist During Last Sync 0 Theme -> Nickname Format 0 Theme -> Nickname Format Preference Enabled 0 Theme -> Timestamp Format 0 Theme -> Timestamp Format Preference Enabled 0 Theme -> User Style Sheet Rules 0 TrackNicknameHighlightsOfLocalUser 0 TrackUserAwayStatusMaximumChannelSize 0 TPCPreferencesDictionaryVersion 0 TPCPreferences -> Migration -> Appearance (7011) 0 TPCPreferences -> Migration -> Sparkle (601) 0 TPCPreferences -> Migration -> Nickname Color Style Overrides 0 TPCPreferences -> Migration -> World Controller Migrated (600) 0 Sandbox Migration -> Migrated Resources 0 Sandbox Migration -> Installation Migrated 0 Sandbox Migration -> User Acknowledged 0 Sandbox Migration -> User Prefers Pruning Files 0 Sandbox Migration -> All Extensions Pruned 0 Sandbox Migration -> Imported Keys 0 TLOLicenseManagerAuthorizationCode 0 THOPluginManager -> Extras Installer Last Check for Update Payload 0 TVCLogControllerHistoricLogFileSavePath_v3 0 TXRunCount 0 TXRunTime 0 User List Mode Badge Colors -> +a 0 User List Mode Badge Colors -> +h 0 User List Mode Badge Colors -> +o 0 User List Mode Badge Colors -> +q 0 User List Mode Badge Colors -> +v 0 User List Mode Badge Colors -> +y 0 User List Mode Badge Colors -> no mode 0 UserListDoubleClickAction 0 UsesWebKit2WhenAvailable 0 WebViewDoNotUsesCustomScrollers 0 WebViewPreviewLinks 0 WebViewProcessPoolSizeIsLimited 0 Wiki-style Link Parser Extension -> Service Enabled 0 Wiki-style Link Parser Extension -> Link Prefixes 0 Window -> Main Window Is Fullscreen'd 0 Window -> Main Window -> Member List is Visible 0 Window -> Main Window -> Server List is Visible 0 Window -> Main Window -> Server List Selection 0 Window -> Main Window -> Split Channel View Saved Frames 0 World Controller Client Configurations 0 ================================================ FILE: Sources/App/Resources/Property Lists/Preferences/RegisteredUserDefaults.plist ================================================ com.adiumX.AutoHyperlinks.permittedSchemesDefault feed ftp gopher irc ircs itms sftp ssh telnet textual webcal x-man-page NSWindowAutosaveFrameMovesToActiveDisplay WebKitDeveloperExtras __uniquePageGroupID-1.WebKit2ScrollAnimatorEnabled __uniquePageGroupID-1.WebKit2ShouldRespectImageOrientation ================================================ FILE: Sources/App/Resources/Property Lists/Preferences/RegisteredUserDefaultsInContainer.plist ================================================ Appearance 0 ApplyCommandToAllConnections -> amsg ApplyCommandToAllConnections -> away ApplyCommandToAllConnections -> clearall ApplyCommandToAllConnections -> nick AutojoinChannelOnInvite AutojoinDelayBetweenChannelJoins 2.2 AutojoinDelayAfterIdentification 0 AutojoinMaximumChannelJoinCount 2 AutomaticallyAddScrollbackMarker AutomaticallyDisconnectForSleepMode AutomaticallyDetectHighlightSpam AutomaticallyFilterUnicodeTextSpam AutomaticallyReloadCustomThemesWhenTheyChange ChannelNavigationIsServerSpecific ChannelOperatorDefaultLocalization -> Kick Reason Your behavior is not conducive to the desired environment. ChannelViewArrangement 0 CommandReturnSendsMessageAsAction ConfirmApplicationQuit ControlEnterSendsMessage ConversationTrackingIncludesUserModeSymbol CopyTextSelectionOnMouseUp DefaultBanCommandHostmaskFormat 1 DefaultIdentity -> AwayNickname DefaultIdentity -> Nickname Guest DefaultIdentity -> Realname Textual User DefaultIdentity -> Username textual DestinationOfNonserverNotices 0 DisableMainWindowSegmentedController DisableRemoteNicknameColorHashing DisableSidebarTranslucency DisplayDockBadges DisplayEventInLogView -> Date Changes DisplayEventInLogView -> Inline Media DisplayEventInLogView -> Join, Part, Quit DisplayPublicMessageCountInDockBadge DisplayServerMessageOfTheDayOnConnect DisplayUserListNoModeSymbol File Transfers -> File Transfer IP Address Detection Method 1 File Transfers -> File Transfer IP Address Detection Prefers 3rd-party Sources File Transfers -> File Transfer Port Range End 1130 File Transfers -> File Transfer Port Range Start 1115 File Transfers -> File Transfer Request Reply Action 2 File Transfers -> File Transfer Requests Use Reverse DCC File Transfers -> Idle System Sleep Prevented During File Transfer FocusSelectionOnMessageCommandExecution IRCopDefaultLocalizaiton -> G:Line Reason 35d Your behavior is not conducive to the desired environment. IRCopDefaultLocalizaiton -> Kill Reason Your behavior is not conducive to the desired environment. IRCopDefaultLocalizaiton -> Shun Reason 1d Shunned. InlineMediaCheckEverything InlineMediaLimitToBasics InlineMediaLimitBasicsToFiles InlineMediaLimitInsecureContent InlineMediaLimitNaughtyContent InlineMediaLimitUnsafeContent InlineMediaMaximumFilesize 2 InlineMediaMaximumHeight 0 InlineMediaScalingWidth 300 IRC -> Enable echo-message Capability Keyboard -> Command+W Key Action 0 Keyboard -> Tab Key Action 0 LogHighlights LogTranscript MemberListSortFavorsServerStaff MemberListUpdatesUserInfoPopoverOnScroll Main Input Text Field -> Font Size 1 Main Input Text Field -> Focus When Changing Views MainWindowTransparencyLevel 1 NicknameHighlightMatchingType 1 NotificationType -> Address Book Match -> Enabled NotificationType -> Failed File Transfer (Receiving) -> Bounce Dock Icon NotificationType -> Failed File Transfer (Receiving) -> Enabled NotificationType -> Failed File Transfer (Sending) -> Bounce Dock Icon NotificationType -> Failed File Transfer (Sending) -> Enabled NotificationType -> File Transfer Request -> Bounce Dock Icon NotificationType -> File Transfer Request -> Enabled NotificationType -> File Transfer Request -> Sound Blow NotificationType -> Highlight -> Bounce Dock Icon NotificationType -> Highlight -> Enabled NotificationType -> Highlight -> Sound Glass NotificationType -> Private Message (New) -> Bounce Dock Icon NotificationType -> Private Message (New) -> Enabled NotificationType -> Private Message (New) -> Sound Submarine NotificationType -> Private Message -> Bounce Dock Icon NotificationType -> Private Message -> Enabled NotificationType -> Private Message -> Sound Submarine NotificationType -> Public Message -> Speak Channel Name NotificationType -> Public Message -> Speak Nickname NotificationType -> Successful File Transfer (Receiving) -> Bounce Dock Icon NotificationType -> Successful File Transfer (Receiving) -> Enabled NotificationType -> Successful File Transfer (Sending) -> Bounce Dock Icon NotificationType -> Successful File Transfer (Sending) -> Enabled PostNotificationsWhileInFocus PreferModernCiphers PreferModernSockets Off-the-Record Messaging -> Automatically Enable Service Off-the-Record Messaging -> Enable Encryption Off-the-Record Messaging -> Require Encryption OnlySpeakNotificationsForSelection OpenClickedLinksInBackgroundBrowser Optimizations -> Load History Lazily ReceiveBetaUpdates RejoinChannelOnLocalKick ReloadScrollbackOnLaunch RightToLeftTextFormatting RemoveIRCTextFormatting ReplyUnignoredExternalCTCPRequests SaveInputHistoryPerSelection ServerListDoubleClickConnectServer ServerListDoubleClickDisconnectServer ServerListDoubleClickJoinChannel ServerListDoubleClickLeaveChannel ServerListRetainsQueriesBetweenRestarts SetAwayOnScreenSleep ScrollbackMaximumSavedLineCount 15000 ScrollbackMaximumVisibleLineCount 0 SwipeMinimumLength 30 Tab Completion -> Completion Suffix Cut Forward Until Space Tab Completion -> Do Not Use Whitespace for Missing Completion Suffix TextFieldAutomaticGrammarCheck TextFieldAutomaticSpellCheck TextFieldAutomaticSpellCorrection TextFieldSmartCopyPaste TextFieldSmartDashes TextFieldDataDetectors TextFieldSmartLinks TextFieldSmartQuotes TextFieldTextReplacement TextualDeveloperEnvironment Theme -> Font Name Lucida Grande Theme -> Font Size 12 Theme -> Name resource:Simplified Theme -> Nickname Format <%@%n> Theme -> Timestamp Format [%H:%M:%S] TrackNicknameHighlightsOfLocalUser TrackUserAwayStatusMaximumChannelSize 300 User List Mode Badge Colors -> +a BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU0NvbG9yAISECE5TT2JqZWN0AIWEAWMBhARmZmZmg5HtHD8Ag/ypsT4Bhg== User List Mode Badge Colors -> +h BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU0NvbG9yAISECE5TT2JqZWN0AIWEAWMBhARmZmZmgwIrhz2DI9v5PoNQjZc9AYY= User List Mode Badge Colors -> +o BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU0NvbG9yAISECE5TT2JqZWN0AIWEAWMBhARmZmZmg0a2sz6DqMZLPoNt5xs/AYY= User List Mode Badge Colors -> +q BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU0NvbG9yAISECE5TT2JqZWN0AIWEAWMBhARmZmZmgyPbOT8AAAGG User List Mode Badge Colors -> +v BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU0NvbG9yAISECE5TT2JqZWN0AIWEAWMBhARmZmZmg6jGSz6Dj8L1PoNt5xs/AYY= User List Mode Badge Colors -> +y BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU0NvbG9yAISECE5TT2JqZWN0AIWEAWMBhARmZmZmg8HKIT+DH4WrPoOLbGc+AYY= UserListDoubleClickAction 200 UsesWebKit2WhenAvailable WebViewDoNotUsesCustomScrollers WebViewPreviewLinks WebViewProcessPoolSizeIsLimited ================================================ FILE: Sources/App/Resources/Property Lists/StaticStore.plist ================================================ TPCThemeController Remapped Themes resource:Simplified Light resource:Simplified resource:Simplified Dark resource:Simplified resource:Tomorrow Night (Eighties) resource:Tomorrow IRCClient List of NickServ Successfully Identified Tokens now recognized automatically identified already identified successfully identified you are already logged in you are now identified password accepted IRCClient List of NickServ Needs Identification Tokens nickname is owned nickname is registered owned by someone else nick belongs to another user if you do not change your nickname authentication required authenticate yourself identify yourself type /msg NickServ IDENTIFY password IRCClient List of Nicknames that Encryption Forbids UnderNet c w x x@channels.undernet.org -default- authserv botserv chanserv hostserv memoserv nickserv operserv rootserv statserv userserv THOPluginManager List of Forbidden Commands connect die error info kill links lusers motd names oper pass ping pong rehash restart server service servlist squery squit stats time trace userhost users version whowas THOPluginManager List of Forbidden Extensions com.codeux.app-extensions.textual-Caffeine THOPluginManager List of Reserved Commands apps banhammer delkey ffuu flip hermes key keyx music np o_p page qt radium setkey setkeymode shell slap spotify vlc THOPluginManager Extras Installer Latest Extension Versions com.codeux.app-extensions.textual-BlowfishEncryption 1.0.18 com.codeux.app-extensions.textual-WikiStyleLinkParser 1.0.9 Spelling Ignores deop ================================================ FILE: Sources/App/Resources/Property Lists/TemplateLineTypes.plist ================================================ action newMessagePostedWithSender ctcp newMessagePostedWithSender dcc-file-transfer newMessagePostedWithoutSender debug newMessagePostedWithoutSender invite newMessagePostedWithoutSender join newMessagePostedWithoutSender kick newMessagePostedWithoutSender kill newMessagePostedWithoutSender mode newMessagePostedWithoutSender nick newMessagePostedWithoutSender notice newMessagePostedWithSender off-the-record-encryption-status newMessagePostedWithoutSender part newMessagePostedWithoutSender privmsg newMessagePostedWithSender quit newMessagePostedWithoutSender topic newMessagePostedWithoutSender website newMessagePostedWithoutSender ================================================ FILE: Sources/App/Resources/Scripting/Script Files/Bundled Scripts/Installing Custom Scripts.txt ================================================ Custom scripts can be installed in the folder: "~/Library/Application Scripts/com.codeux.apps.textual/" This folder might not exist. If it does not, create it. Textual does not have write permission to this location so it cannot create it on its own. Scripts installed within this folder benefit from the ability to be ran outside of the OS X sandbox giving them unrestricted access to the system as a whole. Only OS X Mountain Lion or later can read scripts from this folder. This is an unfortunate design flaw of the OS X sandbox that Textual cannot circumvent. For more information, read the knowledge base: https://help.codeux.com/textual/Writing-Scripts.kb ================================================ FILE: Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/Create-Installers ================================================ set -e export BUILD_PATH="/private/tmp/Textual-${RANDOM}" export BUILD_PATH_SCRIPTS_STANDALONE="${BUILD_PATH}/Library/Application Scripts/com.codeux.apps.textual" export BUILD_PATH_EXTENSIONS_STANDALONE="${BUILD_PATH}/Library/Group Containers/8482Q6EPL6.com.codeux.apps.textual/Library/Application Support/Textual/Extensions" export CURRENT_DIRECTORY=$(cd `dirname $0` && pwd) export SOURCE_FILES_SCRIPTS="${CURRENT_DIRECTORY}/Installation Files/Scripts" export SOURCE_FILES_EXTENSIONS="${CURRENT_DIRECTORY}/Installation Files/Extensions" export PACKAGES_DESTINATION="${CURRENT_DIRECTORY}/Packages" echo "**************************" "${CURRENT_DIRECTORY}/Installer Helpers/Build-Scripts-Package" "AudioVideo" "hermes,music,np,qt,radium,spotify,vlc" echo "**************************" "${CURRENT_DIRECTORY}/Installer Helpers/Build-Scripts-Package" "Fun" "banhammer,ffuu,o_o,slap" echo "**************************" "${CURRENT_DIRECTORY}/Installer Helpers/Build-Scripts-Package" "Utilities" "apps,flip,page,reverse,shell" echo "**************************" "${CURRENT_DIRECTORY}/Installer Helpers/Build-Extension-Package" "BlowfishEncryption" echo "**************************" "${CURRENT_DIRECTORY}/Installer Helpers/Build-Extension-Package" "WikiStyleLinkParser" echo "**************************" "${CURRENT_DIRECTORY}/Installer Helpers/Build-Final-Package" ================================================ FILE: Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/Installer Helpers/Build-Extension-Package ================================================ echo "Extension - ${1} - Start"; codesign -f -s "Developer ID Application: Codeux Software, LLC (8482Q6EPL6)" "${SOURCE_FILES_EXTENSIONS}/${1}.bundle" # ***************************************************** # STANDALONE # ***************************************************** mkdir -p "${BUILD_PATH_EXTENSIONS_STANDALONE}" cp -Rp "${SOURCE_FILES_EXTENSIONS}/${1}.bundle" "${BUILD_PATH_EXTENSIONS_STANDALONE}" pkgbuild \ --root "${BUILD_PATH}" \ --ownership recommended \ --sign "Developer ID Installer: Codeux Software, LLC (8482Q6EPL6)" \ --quiet \ --identifier "com.codeux.app-extensions.textual-extension-installer-${1}" \ --version "1.0" \ "${PACKAGES_DESTINATION}/STANDALONE/Extension-${1}.pkg" rm -fr "${BUILD_PATH}" # ***************************************************** echo "Extension - ${1} - End"; ================================================ FILE: Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/Installer Helpers/Build-Final-Package ================================================ echo "Build Finale Package - Start" # ***************************************************** # STANDALONE # ***************************************************** productbuild \ --distribution distribution.plist \ --resources "./Installer Resources" \ --package-path "${PACKAGES_DESTINATION}/STANDALONE/" \ --sign "Developer ID Installer: Codeux Software, LLC (8482Q6EPL6)" \ --identifier "com.codeux.app-extensions.textual-extras-installer" \ --version "1.0" \ --quiet \ "${PACKAGES_DESTINATION}/Textual-Extras.pkg" rm -f "${PACKAGES_DESTINATION}/STANDALONE/*.pkg" echo "Build Finale Package - End" ================================================ FILE: Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/Installer Helpers/Build-Scripts-Package ================================================ echo "${1} - Start"; # ***************************************************** # STANDALONE # ***************************************************** mkdir -p "${BUILD_PATH_SCRIPTS_STANDALONE}" for i in $(echo $2 | sed "s/,/ /g") do echo "Copying ${i}" cp "${SOURCE_FILES_SCRIPTS}/${3}${i}.scpt" "${BUILD_PATH_SCRIPTS_STANDALONE}" done pkgbuild \ --root "${BUILD_PATH}" \ --ownership recommended \ --sign "Developer ID Installer: Codeux Software, LLC (8482Q6EPL6)" \ --quiet \ --identifier "com.codeux.app-extensions.textual-scripts-installer-${1}" \ --version "1.0" \ "${PACKAGES_DESTINATION}/STANDALONE/Scripts-${1}.pkg" rm -fr "${BUILD_PATH}" # ***************************************************** echo "${1} - End"; ================================================ FILE: Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/Installer Resources/en.lproj/Welcome.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf2571 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;\f1\fnil\fcharset0 HelveticaNeue-Bold;} {\colortbl;\red255\green255\blue255;\red252\green41\blue19;} {\*\expandedcolortbl;;\cssrgb\c100000\c25745\c7993;} \margl1440\margr1440\vieww13040\viewh11540\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 \f0\fs26 \cf0 Click \'93Continue\'94 to view a list of addons that can be installed. \ \ When the next screen appears, expand the various categories,\ then click each item to view its description. \ \ Enable the checkbox next to the items that you are interested in \ then click \'93Install\'94 to complete installation.\ \ \'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\'97\ \ \f1\b \cf2 The macOS Installer may bug out at times and install files \ at the incorrect location. \f0\b0 \cf0 \ \ \f1\b If you perform an installation and find what you selected \ was not installed, then try following these steps: \f0\b0 \ \ 1. When presented with the button \'93Change Install Location\'85\'94\ after the selection screen, click it.\ 2. In the window that appears, click \'93Install for me only.\'94\ 3. Continue installation.} ================================================ FILE: Sources/App/Resources/Scripting/Script Files/Textual Extras Installer/distribution.plist ================================================ DISTRIBUTION_TITLE #Scripts-AudioVideo.pkg #Scripts-Fun.pkg #Scripts-Utilities.pkg #Extension-BlowfishEncryption.pkg #Extension-WikiStyleLinkParser.pkg ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Astria/Templates/encryptedMessageLock.mustache ================================================ [encrypted] ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Astria/copyright.txt ================================================ Copyright © 2010, 2011, 2012 Alex Sørlie. ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Astria/design.css ================================================ /* Inline media */ @import "inlineMedia.css"; /* Basic Body Structure */ :root { supported-color-schemes: dark; } * { margin: 0; padding: 0; font-size: 100%; word-wrap: break-word; word-break: break-word; } body { color: #fff; height: 100%; z-index: 100; font-size: 9px; overflow: hidden; background-color: #000; font-family: "Lucida Grande"; } #body { left: 0; right: 0; bottom: 0; width: 100%; max-height: 100%; overflow-y: auto; z-index: 100; position: absolute; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Only factor in height of topic bar when selected == true because that is only time the topic is displayed according to the logic defined by baseLayout.css */ body[data-selected="true"][data-view-type="channel"] #body { max-height: calc(100% - 2.0em); /* height minus topic */ } div.line { margin-top: -1px; clear: both; } body[dir="rtl"] .sender { display: inline-block; } /* Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #393939; box-shadow: inset 1px 0px 0px 0px #4b4b4b; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #7c7c7c; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #b0b0b0; } /* Loading Screen */ #loadingScreen { position: absolute; top: 45%; left: calc(50% - (320px / 2)); /* half of width + padding */ width: 300px; font-size: 18px; background: #181818; border: 1px solid #333333; border-radius: 5px; padding: 5px 10px; opacity: 1; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Time */ body[dir="ltr"] .time { color: #ffffff; white-space: nowrap; } body[dir="rtl"] .time { color: #ffffff; white-space: nowrap; padding-left: 0.4em; display: inline-block; } /* Encryption Lock */ .encryptionLock img { float: right; margin: 0; padding: 0; height: 11px; margin-top: 2px; padding-left: 10px; } /* Links */ a { color: #0080FF; border-color: #0080FF; text-decoration: none; border-bottom: dotted 1px; } a:hover { color: #40A0FF; border-color: #40A0FF; } /* Topic Bar */ #topicBar { top: 0; left: 0; right: 0; color: #fff; z-index: 400; opacity: 0; /* Set by JavaScript */ position: fixed; background: #000; padding: 2px 0.5em 3px; border-bottom: 1px solid #1f1f1f; -webkit-box-shadow: 0 1px 5px #777; -webkit-transition: opacity 0.8s linear; -webkit-font-smoothing: subpixel-antialiased; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* Topic bar hover additions contributed with permission from the project: */ #topicBar:hover { overflow: visible; white-space: normal; } #topicBar a { color: #0080FF; border-color: #0080FF; text-decoration: none; border-bottom: dotted 1px; } #topicBar a:hover { color: #40A0FF; border-color: #40A0FF; } /* Remember Line */ #mark { clear: both; position: relative; z-index: 295; margin-top: -1px; border-bottom: 1px dashed; border-color: #444; -webkit-transition: 0.2s linear; } /* Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #5a5a5a; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: 0.5em 0; } .date_indicator > hr, .session_indicator > hr { background: #606060; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; color: #d8d8d8; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* NOTICE/CTCP/WALLOPS */ div.line[data-line-type="ctcp"], div.line[data-line-type="notice"], div.line[data-line-type="wallops"] { color: #f00; z-index: 191; background: #400; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #811; border-bottom: 1px solid #811; } div.line[data-line-type="notice"] .sender { color: #f00; font-weight: 700; } /* Selected User Messages */ .sender { cursor: pointer; } div.line[data-line-type="privmsg"]:not(.selectedUser), div.line[data-line-type="action"]:not(.selectedUser) { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; } div.line[data-line-type="privmsg"] .time:not(.selectedUser), div.line[data-line-type="action"] .time:not(.selectedUser) { transition: color 0.5s; } div.line.selectedUser[data-highlight="false"] .time { transition: color 0.5s ease-in; color: #aaa; } div.line.selectedUser[data-line-type="privmsg"][data-member-type="myself"] .time { color: #fff; } div.line.selectedUser[data-highlight="false"] { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; z-index: 190; position: relative; border-top: 1px solid #CC7A03 !important; border-bottom: 1px solid #CC7A03 !important; background-color: rgba(244, 151, 75, 0.25) !important; } /* PRIVMSG */ div.line[data-line-type="privmsg"][data-highlight="false"] { padding: 2px 5px 2px 5px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: none; } div.line[data-line-type="privmsg"][data-highlight="true"], div.line[data-line-type="action"][data-highlight="true"] { z-index: 191; position: relative; font-weight: normal; padding: 2px 5px 2px 5px; border-top: 1px solid #06c793; border-bottom: 1px solid #06c793; background-color: rgba(18, 93, 53, 0.6) !important; } div[data-line-type="privmsg"] .message { color: #aaa; } div.line[data-line-type="privmsg"][data-member-type="myself"] .message { color: #fff; } div.line[data-line-type="privmsg"] .sender { font-weight: 700; white-space: pre-wrap; } /* ACTION */ div.line[data-line-type="action"] { padding: 2px 5px 2px 5px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: none; } div[data-line-type="action"] .message { color: #00ff9c; } div.line[data-line-type="action"] .sender { font-weight: 700; } div.line[data-line-type="action"][data-member-type="myself"] .sender, div.line[data-line-type="action"][data-member-type="myself"] .message { color: #00ff9c; } body[dir="ltr"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-left: 0.4em; */ } /* DEBUG/INVITE */ div.line[data-line-type="invite"], div.line[data-line-type="debug"], div.line[data-line-type="dcc-file-transfer"], div.line[data-line-type="off-the-record-encryption-status"] { color: #777; z-index: 190; background: #222; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #444; border-bottom: 1px solid #444; } /* off-the-record-encryption-status Message Event */ div.line[data-line-type="off-the-record-encryption-status"] .message { color: #ff0000; font-weight: 700; } /* Message of the Day (MOTD) */ /* 720, 721, 722 are used by ShadowIRCd for Oper MOTD. */ /* 372, 375, 376 are normal MOTD shared by several IRCds. */ div.line[data-command="372"], div.line[data-command="721"] { border: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="375"], div.line[data-command="720"] { /* Start. */ border-bottom: none; padding-top: 2px; padding-bottom: 3px; } div.line[data-command="376"], div.line[data-command="722"] { /* End. */ border-top: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="372"] .message, div.line[data-command="375"] .message, div.line[data-command="376"] .message div.line[data-command="720"] .message, div.line[data-command="721"] .message, div.line[data-command="722"] .message { font-family: "Menlo" !important; } /* GENERAL EVENT */ div.line[data-line-type="join"], div.line[data-line-type="part"], div.line[data-line-type="kick"], div.line[data-line-type="quit"], div.line[data-line-type="kill"], div.line[data-line-type="nick"], div.line[data-line-type="mode"], div.line[data-line-type="website"], div.line[data-line-type="topic"] { padding: 3px 5px 3px 5px; color: #666; } body[dir="ltr"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="join"] .message { color: #b589ff; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="kick"] .message:before { content: "✕"; color: #e00; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="part"] .message:before, body[dir="ltr"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="kick"] .message { color: #ff3d3d; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="part"] .message { color: #ffae01; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="quit"] .message { color: #ffee7a; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="nick"] .message:before { content:"•"; color: #0090ff; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="nick"] .message { color: #0090ff; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="mode"] .message:before { content: "❖"; color: #b4a355; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="mode"] .message { color: #9c9c9c; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="topic"] .message:before { content: "✦"; color: #00a2ff; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="topic"] .message { color: #00ffa2; margin-right: 0.4em; } body[dir="rtl"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="join"] .message { color: #b589ff; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="kick"] .message:before { content: "✕"; color: #e00; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="part"] .message:before, body[dir="rtl"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="kick"] .message { color: #ff3d3d; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="part"] .message { color: #ffae01; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="quit"] .message { color: #ffee7a; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="nick"] .message:before { content:"•"; color: #0090ff; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="nick"] .message { color: #0090ff; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="mode"] .message:before { content: "❖"; color: #b4a355; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="mode"] .message { color: #9c9c9c; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="topic"] .message:before { content: "✦"; color: #00a2ff; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="topic"] .message { color: #00ffa2; margin-left: 0.4em; } /* Nickname Colors */ .inlineSender { font-weight: 700; } div.line[data-line-type="privmsg"] .sender[data-member-type="myself"] { color: #B8DFFF; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Astria/inlineMedia.css ================================================ /* Images */ .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin-right: 12px; margin-left: 10px; min-width: 40px; max-width: 90%; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { cursor: pointer; border-radius: 5px; border: 2px solid #a1a1a1; color: #a1a1a1; display: inline-block; line-height: 14px; font-size: 15px; font-family: "Helvetica Neue" !important; text-indent: 7px; width: 16px; height: 16px; float: left; padding-right: 7px; padding-left: 0px; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Astria/scripts.js ================================================ /* Defined in: "Textual.app -> Contents -> Resources -> JavaScript -> API -> core.js" */ Textual.viewBodyDidLoad = function() { Textual.fadeOutLoadingScreen(1.00, 0.95); } Textual.messageAddedToView = function(line, fromBuffer) { var element = document.getElementById("line-" + line); ConversationTracking.updateNicknameWithNewMessage(element); } Textual.nicknameSingleClicked = function(e) { ConversationTracking.nicknameSingleClickEventCallback(e); } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Astria/settings.plist ================================================ Appearance dark Channel View Overlay Color #00000066 Indentation Offset 6 Nickname Color Style HSL-dark Template Engine Versions default 4 Underlying Window Color #000000 ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Equinox/LICENSE.txt ================================================ Copyright (c) 2013-2015, April King, Alex Sørlie Glomsaas, Tobias Pollmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Equinox/Templates/encryptedMessageLock.mustache ================================================ [encrypted] ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Equinox/design.css ================================================ /* Inline media */ @import "inlineMedia.css"; /* Basic Body Structure */ :root { supported-color-schemes: dark; } * { margin: 0; padding: 0; font-size: 100%; word-break: break-word; } body { color: #f2f2f2; z-index: 100; font-size: 12px; background-color: #1d1d1d; font-family: "Helvetica Neue"; font-weight: 200; overflow-y: auto; } p { overflow: visible !important; } body .line { clear: both; } #body { left: 0; right: 0; bottom: 0; position: absolute; width: 100%; z-index: 100; max-height: 99.99%; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } #body { max-height: 100%; } body[dir=rtl] .senderContainer { display: inline-block; } /* Hide some internal stuff. */ #timestampWidth { display: none; } /* Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #393939; box-shadow: inset 1px 0px 0px 0px #4b4b4b; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #7c7c7c; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #b0b0b0; } /* Loading Screen */ #loadingScreen { position: fixed; margin: auto; top: 0; left: 0; right: 0; bottom: 0; width: 210px; height: 35px; line-height: 35px; text-align: center; font-size: 18px; background: #363636; padding: 5px; padding-left: 10px; opacity: 1; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } #loadingScreen progress { margin-top: 5px; } /* Time */ body[dir=ltr] .time { font-size: .91em !important; white-space: nowrap; float: right; color: #999; -webkit-user-select: none; /* font-weight: bold; */ margin-right: 10px; padding-left: 25px; line-height: 22px; font-style: normal; text-align: right; } /* Encryption Lock */ .encryptionLock { } .encryptionLock img { z-index: 100; position:absolute; top:0; left: 5px; bottom:0; margin:auto; height: 12px; width: auto; } /* Links */ a { color: #ccc; border-color: #ccc; text-decoration: none; border-bottom: dotted 1px; } a:hover { color: #64a5ff; border-color: #64a5ff; } /* Topic Bar */ #topicBar { top: 0; left: 0; right: 0; padding-bottom: 5px; opacity: 0; /* Set by JavaScript */ z-index: 400; color: #dddddd; overflow: hidden; position: fixed; padding: 7px 0.5em 7px; box-shadow: 0 1px 5px #777; border-bottom: 1px solid #404040; -webkit-transition: opacity 0.8s linear; text-shadow: 1px 1px rgba(10, 10, 10, 0.7); background: rgba(60, 60, 60, 0.91); } #topicBar a, #topicBar .channel { color: #64a5ff; border-color: #64a5ff; text-shadow: 1px 1px rgba(10, 10, 10, 0.7); } /* Hidden scrollback history bar */ #scrolling_history { z-index: 400; position: fixed; bottom: 0; left: 0; right: 0; height: auto; box-shadow: 0 1px 5px #777; border-top: 1px solid #404040; text-shadow: 1px 1px rgba(10, 10, 10, 0.7); background: rgba(31, 51, 81, 0.95); display: none; overflow: hidden; } #scrolling_history .senderContainer { background: rgba(31, 51, 81, .95); } /* Remember Line */ #mark { position: relative; z-index: 295; margin-top: -1px; border-bottom: 1px solid; border-color: #444; -webkit-transition: 0.2s linear; } body[dir=ltr] div[data-line-type=action] .sender:before { margin-right: 0.6em; font-weight: bold; font-size: 80% !important; content: "●"; } /* Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #5a5a5a; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: .5em 0; } .date_indicator > hr, .session_indicator > hr { background: #444; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* NOTICE/CTCP/WALLOPS */ body div.line[data-line-type=ctcp], body div.line[data-line-type=notice], body div.line[data-line-type=wallops] { z-index: 191; position: relative; border-bottom: none; border-top: none; border-left: none; background-color: #1d2734; } body div[data-line-type=ctcp] .senderContainer, body div[data-line-type=notice] .senderContainer { color: #f00; font-weight: 700; min-height:23px; width: 134px; padding-right:7px; text-align:right; display:inline-block; overflow: auto; position: absolute; top: 0; bottom: 0; left: 0; text-overflow: ellipsis; background-color: #161616; } body div[data-line-type=notice] .message { color: #f2f2f2; } /* PRIVMSG */ body div.line[data-line-type=privmsg][data-highlight=false] { padding: 0; position: relative; min-height: 23px; } body div[data-line-type=privmsg] .senderContainer { /* font-weight: 700; */ min-height:23px; width: 134px; border-right: 1px solid rgb(64, 64, 64); padding-right:7px; background-color: #161616; text-align:right; display:inline-block; overflow: hidden; position: absolute; top: 0; bottom: 0; left: 0; text-overflow: ellipsis; } body .senderContainer .swrapper { font-size: 0px !important; opacity: 0; } body div[data-line-type="privmsg"] .message { color: #f2f2f2; } body div[data-line-type="privmsg"][data-encrypted="true"] { background: repeating-linear-gradient(-45deg, rgba(79, 79, 79, 0.1), rgba(79, 79, 79, 0.1) 20px, rgba(68, 89, 63, 0.3) 20px, rgba(68, 89, 63, 0.3) 40px) !important; } body div[data-line-type="privmsg"][data-encrypted="failed"] { background: repeating-linear-gradient(-45deg, rgba(45, 6, 6, 0.2), rgba(45, 6, 6, 0.2) 20px, rgba(104, 15, 15, 0.2) 20px, rgba(104, 15, 15, 0.2) 40px) !important; } body div[data-line-type="privmsg"][data-highlight=true][data-encrypted="true"] { background: repeating-linear-gradient(-45deg, #30473a, #30473a 20px, rgba(68, 89, 63, 0.3) 20px, rgba(68, 89, 63, 0.3) 40px) !important; } body div[data-line-type=privmsg][data-line-type=myself] .message { color: #f2f2f2; } body div[data-line-type=privmsg] span.effect[data-foreground-color=1]:not([data-background-color]) { color: #f2f2f2; /* make black text with no bg white for readability */ } /* ACTION */ body div[data-line-type=action] .senderContainer { position: absolute; top: 0; bottom: 0; left: 0; width: 134px; /* font-weight: 700;*/ border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: #1d1d1d; height:22px; padding-right: 7px; text-align:right; display:inline-block; overflow: hidden; } body div.line[data-line-type=action] .message { padding-top: 1px; } body div.line[data-line-type=action][data-highlight=true] .message { color: #f2f2f2 !important; } /* DEBUG/INVITE */ body div.line[data-line-type=invite], body div.line[data-line-type=debug], body div.line[data-line-type=dcc-file-transfer], body div.line[data-line-type=off-the-record-encryption-status] { color: #f2f2f2; z-index: 190; background: #2f2f2f; position: relative; padding: 2px 0 2px 0; border-top: 1px solid #393939; border-bottom: 1px solid #393939; } body div.line[data-line-type=ctcp] .message, body div.line[data-line-type=invite] .message, body div.line[data-line-type=debug] .message, body div.line[data-line-type=dcc-file-transfer] .message, body div.line[data-line-type=off-the-record-encryption-status] .message { text-indent: 0; padding-left: 145px !important; } /* off-the-record-encryption-status Message Event */ body div.line[data-line-type=off-the-record-encryption-status] .message { color: #ff0000; font-weight: 700; } /* Message of the Day (MOTD) */ /* 720, 721, 722 are used by ShadowIRCd for Oper MOTD. */ /* 372, 375, 376 are normal MOTD shared by several IRCds. */ body div.line[data-command="372"], body div.line[data-command="721"] { border: none; padding-top: 3px; padding-bottom: 3px; } body div.line[data-command="375"], body div.line[data-command="720"] { /* Start. */ border-bottom: none; padding-top: 2px; padding-bottom: 3px; } body div.line[data-command="376"], body div.line[data-command="722"] { /* End. */ border-top: none; padding-top: 3px; padding-bottom: 3px; } body div.line[data-command="372"] .message, body div.line[data-command="375"] .message, body div.line[data-command="376"] .message body div.line[data-command="720"] .message, body div.line[data-command="721"] .message, body div.line[data-command="722"] .message { font-family: "Menlo" !important; } /* GENERAL EVENT */ body span.message { position: relative; padding-left: 150px; /* padding-right: 25px; */ display: table-cell; min-height: 22px; line-height: 22px; overflow: visible !important; } body div.event span.time { color: #999; line-height: 19px; display: table-cell; } body div.event span.message { min-height: 19px; line-height: 19px; text-indent: -20px; padding-left: 0; } body div.line[data-line-type=join], body div.line[data-line-type=part], body div.line[data-line-type=quit], body div.line[data-line-type=mode], body div.line[data-line-type=nick], body div.line[data-line-type=topic], body div.line[data-line-type=kill], body div.line[data-line-type=kick], body div.line[data-line-type=website] { font-size: 85%; min-height: 19px; padding-left: 145px; color: #a5b6ce; background: #1d1d1d; } body div.line[data-line-type=topic] span.message { text-indent: 0; } body div.line[data-line-type=nick] { color: #529567; } body div.line[data-line-type=kill], body div.line[data-line-type=kick] { color: #ec5151; } body[dir=ltr] div[data-line-type=join] .message:before { content: "→"; color: #0c0; margin-left:3px; margin-right: 5px; } body[dir=ltr] div[data-line-type=kick] .message:before, body[dir=ltr] div[data-line-type=part] .message:before, body[dir=ltr] div[data-line-type=quit] .message:before { content: "←"; color: #e00; margin-left:3px; margin-right: 5px; } body[dir=ltr] div[data-line-type=nick] .message:before { content: "\2022"; color: #37be34; font-weight: 700; margin-left: 8px; margin-right: 5px; } body[dir=ltr] div[data-line-type=mode] .message:before { content: "\2022"; color: #2cb6f6; font-weight: 700; margin-left: 8px; margin-right: 5px; } body[dir=rtl] div[data-line-type=join] .message:before { content: "→"; color: #0c0; margin-left:3px; margin-right: 5px; } body[dir=rtl] div[data-line-type=kick] .message:before, body[dir=rtl] div[data-line-type=part] .message:before, body[dir=rtl] div[data-line-type=quit] .message:before { content: "←"; color: #e00; margin-left:3px; margin-right: 5px; } /* Nickname Colors */ body .sender[data-member-type=myself] { color: #9A60FF; } body .sender { margin-right: -.25em; } body .f { /* fade out */ -webkit-transition: color .75s; color: rgb(22, 22, 22) !important; } /* SELECTED USER MESSAGES */ .senderContainer { cursor: pointer; overflow: hidden; white-space: nowrap; } body div.line[data-line-type=privmsg]:not(.selectedUser), body div.line[data-line-type=action]:not(.selectedUser) { transition-property: background-color; transition-duration: 0.5s, 0.5s, 0.5s; } body div.line[data-line-type=privmsg] .time:not(.selectedUser), body div.line[data-line-type=action] .time:not(.selectedUser) { transition: color 0.5s; } body div.line.selectedUser[data-highlight=false] .time { transition: color 0.5s ease-in; color: #aaa; } body div.line.selectedUser[data-highlight=false] { transition-property: background-color; transition-duration: 0.5s, 0.5s, 0.5s; z-index: 190; position: relative; background-color: #2f3e53; } body div.line[data-line-type=privmsg][data-highlight=true], body div.line[data-line-type=action][data-highlight=true] { z-index: 191; position: relative; font-weight: normal; background-color: #30473a; } /* Shamelessly steal the background colors from Tomorrow Night */ .effect[data-foreground-color='0'] { color: #EAEAEA !important; } .effect[data-foreground-color='1'] { color: #515151 !important; } .effect[data-foreground-color='2'] { color: #6699CC !important; } .effect[data-foreground-color='3'] { color: #99CC99 !important; } .effect[data-foreground-color='4'] { color: #F2777A !important; } .effect[data-foreground-color='5'] { color: #FF9DA4 !important; } .effect[data-foreground-color='6'] { color: #CC99CC !important; } .effect[data-foreground-color='7'] { color: #F99157 !important; } .effect[data-foreground-color='8'] { color: #FFCC66 !important; } .effect[data-foreground-color='9'] { color: #B9CA4A !important; } .effect[data-foreground-color='10'] { color: #66CCCC !important; } .effect[data-foreground-color='11'] { color: #99FFFF !important; } .effect[data-foreground-color='12'] { color: #7AA6DA !important; } .effect[data-foreground-color='13'] { color: #C397D8 !important; } .effect[data-foreground-color='14'] { color: #999999 !important; } .effect[data-foreground-color='15'] { color: #CCCCCC !important; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Equinox/inlineMedia.css ================================================ /* Images */ body a img { border: 0; } body video, body img { display: block; } .inlineImage, .inlineVideo { position: relative; display: table; overflow: auto; width: auto; height: auto; } .inlineImage > a, .inlineVideo > a { display: inline-block; border: none; } .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin: 15px 15px 12px 10px; min-width: 100px; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { background: #000000; cursor: pointer; border-radius: 50%; position: absolute; box-sizing: border-box; font-size: 20px !important; font-family: "Helvetica Neue" !important; font-weight: 100 !important; text-align: center; line-height: 17px; color: #f2f2f2; border: 2px solid #f2f2f2; top: 5px; right: 5px; width: 25px; height: 25px; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Equinox/scripts.js ================================================ /* jslint browser: true */ /* global app, Textual */ /* Defined in: "Textual.app -> Contents -> Resources -> JavaScript -> API -> core.js" */ /* Theme-wide preferences, as per milky's request */ var Equinox = { fadeNicks: true, // fade out nicknames when they appear multiple times in a row fadeNicksFreq: 10, // how frequently to display a nick if they have fadeNickCounts lines in a row squashModes: true, // if a duplicate mode gets posted to the channel, squash it squashTopics: true // if a duplicate topic gets posted to the channel, squash it }; /* Set the default statuses for everything tracked in the roomState */ var mappedSelectedUsers = []; var rs = { // room state channelJoined: false, enableHistoryView: false, // this doesn't get enabled until the history has finished loading mode: { mode: undefined }, nick: { count: 1, delete: false, id: undefined, nick: undefined }, nickname: null, topic: { delete: false, topic: undefined } }; /* State tracking for client information */ Equinox.refreshLocalNicknameCache = function() { 'use strict'; app.localUserNickname( function(returnValue) { rs.nickname = returnValue; } ); }; Equinox.refreshChannelJoinedCache = function() { 'use strict'; app.channelIsJoined( function(returnValue) { rs.channelJoined = returnValue; } ); }; /* Nickname colors */ var NickColorGenerator = (function () { 'use strict'; function NickColorGenerator(message) { var i, inlineNicks, nick; // Start alternative nick colouring procedure var selectNick = message.querySelector('.sender'); inlineNicks = message.querySelectorAll('.inlineSender'); this.generateColorFromNickname(selectNick.dataset.nickname, function(nickcolor) { selectNick.style.color = nickcolor; if (message.dataset.lineType === 'action') { message.querySelector('.message').style.color = nickcolor; } } ); var self = this; for (i = 0; i < inlineNicks.length; i++) { nick = inlineNicks[i].textContent; if (inlineNicks[i].dataset.mode.length > 0) { nick = nick.replace(inlineNicks[i].dataset.mode, ''); } var inlineNick = inlineNicks[i]; (function(inlineNickname) { self.generateColorFromNickname(nick, function(nickcolor) { inlineNickname.style.color = nickcolor; } ); })(inlineNick); } } NickColorGenerator.prototype.generateColorFromNickname = function (nick, callbackFunction) { // First, sanitize the nicknames nick = nick.toLowerCase(); // make them lowercase (so that April and april produce the same color) nick = nick.replace(/[`_-]+$/, ''); // typically `, _, and - are used on the end of a nick nick = nick.replace(/|.*$/, ''); // remove | from the end // Generate the hashes app.nicknameColorStyleHash(nick, 'HSL-dark', function(hhash) { var shash = hhash >>> 1; var lhash = hhash >>> 2; var h = hhash % 360; var s = shash % 50 + 45; // 50 - 95 var l = lhash % 36 + 45; // 45 - 81 // give the pinks a wee bit more lightness if (h >= 280 && h < 335) { l = lhash % 36 + 50; // 50 - 86 } // Give the blues a smaller (but lighter) range if (h >= 210 && h < 280) { l = lhash % 25 + 65; // 65 - 90 } // Give the reds a bit less saturation if (h <= 25 || h >= 335) { s = shash % 33 + 45; // 45 - 78 } // Give the yellows and greens a bit less saturation as well if (h >= 50 && h <= 150) { s = shash % 50 + 40; // 40 - 90 } var nickcolor = 'hsl(' + String(h) + ',' + String(s) + '%,' + String(l) + '%)'; callbackFunction(nickcolor); } ); }; return NickColorGenerator; })(); function isMessageInViewport(elem) { 'use strict'; if (!elem.getBoundingClientRect) { return true; } // Have to use Math.floor() because sometimes the getBoundingClientRect().bottom is a fraction of a pixel (!!!) return (Math.floor(elem.getBoundingClientRect().bottom) - 1) <= Math.floor(document.documentElement.clientHeight); } function toggleHistoryIfScrolled() { 'use strict'; var line, lines; var topic = document.getElementById('topicBar'); lines = document.getElementById('body').getElementsByClassName('line'); if (lines.length < 2) { return; } line = lines[lines.length - 1]; if (isMessageInViewport(line) === false) { // scrollback rs.history.style.display = 'inline'; if (topic) { topic.style.visibility = 'hidden'; } } else { // at the bottom rs.history.style.display = 'none'; if (topic) { topic.style.visibility = 'visible'; } } } /* When you join a channel, delete all the old disconnected messages */ Textual.handleEvent = function (event) { 'use strict'; var i, messages; var messagesRemovedCount = 0; switch(event) { case "channelJoined": { rs.channelJoined = true; messages = document.querySelectorAll('div[data-command="-100"]'); for (i = 0; i < messages.length; i++) { if (messages[i].getElementsByClassName('message')[0].textContent.search('Disconnect') !== -1) { messages[i].parentNode.removeChild(messages[i]); messagesRemovedCount += 1; } } if (messagesRemovedCount > 0) { MessageBuffer.noteMessagesRemovedFromBuffer(messagesRemovedCount); } break; } case "channelParted": { rs.channelJoined = false; break; } case "nicknameChanged": { Equinox.refreshLocalNicknameCache(); break; } /* It is important to have serverConnected as a state because the nickname may change then, outside of the NICK command (handled by "nicknameChanged"). For example, user can connect to ZNC which has a different nickname than set locally. */ case "serverConnected": { Equinox.refreshLocalNicknameCache(); break; } default: { break; } } // switch() }; Textual.messageAddedToView = function (line, fromBuffer) { 'use strict'; var message = document.getElementById('line-' + line); var messageRemoved; var clone, elem, getEmbeddedImages, i, mode, messageText, sender, topic; // reset the message count and previous nick, when you rejoin a channel if (message.dataset.lineType !== 'privmsg') { rs.nick.count = 1; rs.nick.nick = undefined; } // if it's a private message, colorize the nick and then track the state and fade away the nicks if needed if (message.dataset.lineType === 'privmsg' || message.dataset.lineType === 'action') { sender = message.getElementsByClassName('sender')[0]; if (sender.dataset.overrideColor !== 'true') { new NickColorGenerator(message); // colorized the nick } // Delete (ie, make foreground and background color identical) the previous line's nick, if it was set to be deleted if (rs.nick.delete === true) { elem = document.getElementById(rs.nick.id).getElementsByClassName('sender')[0]; elem.className += ' f'; } // Track the nicks that submit messages, so that we can space out everything if ((rs.nick.nick === sender.textContent) && (rs.nick.count < Equinox.fadeNicksFreq) && (message.dataset.lineType !== 'action') && (Equinox.fadeNicks === true)) { rs.nick.delete = true; rs.nick.count += 1; } else { rs.nick.nick = sender.textContent; rs.nick.count = 1; rs.nick.delete = false; } // Track the previous message's id rs.nick.id = message.getAttribute('id'); // Copy the message into the hidden history clone = message.cloneNode(true); clone.removeAttribute('id'); if (fromBuffer === false) { rs.history.appendChild(clone); } // Colorize it as well if (sender.dataset.overrideColor !== 'true') { new NickColorGenerator(clone); // colorized the nick } // Remove old messages, if the history is longer than three messages if (rs.history.childElementCount > 2) { rs.history.removeChild(rs.history.childNodes[0]); // Hide the first nick in the hidden history, if it's the same as the second if ((rs.nick.count > 1) && (message.dataset.lineType !== 'action')) { rs.history.getElementsByClassName('sender')[0].style.visibility = 'hidden'; } } } /* Let's kill topics that appear where they had already been set before This happens when you join a room (like a reconnect) that you had been in and seen the topic before */ if (Equinox.squashTopics === true && message.dataset.lineType === 'topic') { topic = message.getElementsByClassName('message')[0].textContent.replace('Topic is ', '').replace(/\s+/, ''); if (message.dataset.command === '332') { // an actual topic change // hide the topic if it's the same topic again if (topic === rs.topic.topic) { message.parentNode.removeChild(message); messageRemoved = true; rs.topic.delete = true; } rs.topic.topic = topic; } if ((message.dataset.command === '333') && (rs.topic.delete === true)) { message.parentNode.removeChild(message); messageRemoved = true; rs.topic.delete = false; } } // much like we suppress duplicate topics, we want to suppress duplicate modes if (Equinox.squashModes === true && message.dataset.lineType === 'mode') { mode = message.getElementsByClassName('message')[0].textContent.replace(/\s+/, ''); if (mode === rs.mode.mode) { message.parentNode.removeChild(message); messageRemoved = true; } else { rs.mode.mode = mode; } } // hide messages about yourself joining if ((message.dataset.lineType === 'join') || (message.dataset.lineType === 'part')) { if (rs.nickname == message.getElementsByClassName('message')[0].getElementsByTagName('b')[0].textContent) { message.parentNode.removeChild(message); messageRemoved = true; } } /* clear out all the old disconnect messages, if you're currently connected to the channel note that normally Textual.handleEvent will catch this, but if you reload a theme, they will reappear */ if ((message.dataset.lineType === 'debug') && (message.dataset.command === '-100')) { if (rs.channelJoined && message.getElementsByClassName('message')[0].textContent.search('Disconnect') !== -1) { message.parentNode.removeChild(message); messagesRemoved = true; } } /* Textual's buffer keeps a record of number of messages that appear in the buffer. The buffer counts anything with a line number as a message. If we remove messages, we should let it know so that it can correct any irregularities in its math. */ if (messageRemoved) { MessageBuffer.noteMessageRemovedFromBuffer(); return; } getEmbeddedImages = message.querySelectorAll('img'); if (getEmbeddedImages) { for (i = 0; i < getEmbeddedImages.length; i++) { getEmbeddedImages[i].onload = function (e) { setTimeout(function () { if (e.target.offsetHeight > (window.innerHeight - 150)) { e.target.style.height = (window.innerHeight - 150); } }, 1000); }; } } ConversationTracking.updateNicknameWithNewMessage(message); }; /* This is called when a .sender is clicked */ Textual.nicknameSingleClicked = function (e) { ConversationTracking.nicknameSingleClickEventCallback(e); }; Textual.viewBodyDidLoad = function () { 'use strict'; Textual.fadeOutLoadingScreen(1.00, 0.95); }; Textual.viewFinishedLoadingHistory = function () { 'use strict'; // enable the history view, but only a bit after this gets called setTimeout(function() { rs.enableHistoryView = true; }, 850); } Textual.viewInitiated = function () { 'use strict'; /* When the view is loaded, create a hidden history div which we display if there is scrollback */ var body = document.getElementById('body'), div = document.createElement('div'); div.id = 'scrolling_history'; document.getElementsByTagName('body')[0].appendChild(div); rs.history = div; /* setup the scrolling event to display the hidden history if the bottom element isn't in the viewport also hide the topic bar when scrolling. Note that we have to set a timer here so that the history div doesn't appear in the viewport on normal inserts, which cause scroll effects */ window.addEventListener('scroll', function () { // check to see if a bit of time has passed since we loaded the history if (!rs.enableHistoryView) { return; } rs.scrollTimer = setTimeout(toggleHistoryIfScrolled, 100); }); /* Cache client information so we do not have to wait for callback functions to complete. */ Equinox.refreshChannelJoinedCache(); Equinox.refreshLocalNicknameCache(); }; ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Equinox/settings.plist ================================================ Appearance dark Channel View Overlay Color #00000066 Force Invert Sidebars Nickname Color Style HSL-dark Nickname Format %@%n Post Textual.handleEvent() Notifications Template Engine Versions default 4 Underlying Window Color #232323 ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sapientia/Resources/Documentation/changelog.txt ================================================ #Version Information, "Sapientia". 2012-07-02 - Version 1.3 * Topic bar: - Font size increased (13pt -> 15pt) - Gradient background added. * Override nickname style to %@%n * Support for inverted sidebars in 2.1.1 2012-06-08 - Version 1.2 * Updated for Textual 2.1, fixing bugs caused by the new version. * Brightened background colour, slightly. 2011-04-10 - Version 1.1 * Changed highlight colours. 2011-04-09 - Version 1.0 * First release. ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sapientia/Templates/encryptedMessageLock.mustache ================================================ [encrypted] ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sapientia/copyright.txt ================================================ "Sapientia" is a style by Noah "Brisk" Darville-Jennings (http://www.noahdj.ca/). This style is based on the stripped down version of the "Simplified" theme developed by "Cowboy" Ben Alman (http://benalman.com/). Copyright © 2010, 2011, 2012. ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sapientia/design.css ================================================ /* Inline media */ @import "inlineMedia.css"; /* Basic Body Structure - Sapientia 1.3 */ :root { supported-color-schemes: dark; } * { margin: 0; padding: 0; font-size: 100%; word-wrap: break-word; word-break: break-word; } body { color: #E8E7E3; height: 100%; z-index: 100; font-size: 15pt; overflow: hidden; background-color: #242323; font-family: "HelveticaNeue"; } #body { left: 0; right: 0; bottom: 0; width: 100%; max-height: 100%; overflow-y: auto; z-index: 100; position: absolute; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Only factor in height of topic bar when selected == true because that is only time the topic is displayed according to the logic defined by baseLayout.css */ body[data-selected="true"][data-view-type="channel"] #body { max-height: calc(100% - 2.2em); /* height minus topic */ } div.line { margin-top: -1px; clear: both; } body[dir="rtl"] .sender { display: inline-block; } /* Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #393939; box-shadow: inset 1px 0px 0px 0px #4b4b4b; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #7c7c7c; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #b0b0b0; } /* Loading Screen */ #loadingScreen { position: absolute; top: 45%; left: calc(50% - (320px / 2)); /* half of width + padding */ width: 300px; font-size: 18px; background: #181818; border: 1px solid #333333; border-radius: 5px; padding: 5px 10px; opacity: 1; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Time */ body[dir="ltr"] .time { color: #343434; white-space: nowrap; } body[dir="rtl"] .time { color: #343434; white-space: nowrap; padding-left: 0.4em; display: inline-block; } /* Encryption Lock */ .encryptionLock img { float: right; margin: 0; padding: 0; height: 11px; margin-top: 2px; padding-left: 10px; } /* Links */ a { color: #FF7D40; border-color: #FF7D40; text-decoration: none; border-bottom: dotted 1px; } a:hover { color: #FF4500; border-color: #FF4500; } /* Topic Bar */ #topicBar { top: 0; left: 0; right: 0; color: #a39888; z-index: 400; opacity: 0; /* Set by JavaScript */ line-height: 145%; position: fixed; background: -webkit-linear-gradient(top, #494949 0%, #282828 100%); padding: 2px 0.5em 3px; border-bottom: 1px solid #544E45; -webkit-box-shadow: 0 1px 5px #000; -webkit-transition: opacity 0.8s linear; -webkit-font-smoothing: subpixel-antialiased; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* Topic bar hover additions contributed with permission from the project: */ #topicBar:hover { overflow: visible; white-space: normal; } #topicBar a { color: #FF7D40; border-color: #FF7D40; } #topicBar a:hover { color: #FF4500; border-color: #FF4500; } /* Remember Line */ #mark { clear: both; position: relative; z-index: 295; margin-top: -1px; border-bottom: 1px dashed; border-color: #444; -webkit-transition: 0.2s linear; } /* Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #797979; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: 0.5em 0; } .date_indicator > hr, .session_indicator > hr { background: #444; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; color: #919191; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* NOTICE/CTCP/WALLOPS */ div.line[data-line-type="ctcp"], div.line[data-line-type="notice"], div.line[data-line-type="wallops"] { color: #a00; z-index: 191; background: #300; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #400; border-bottom: 1px solid #400; } div.line[data-line-type="notice"] .sender { color: #700; font-weight: 700; } /* Selected User Messages */ .sender { cursor: pointer; } div.line[data-line-type="privmsg"]:not(.selectedUser), div.line[data-line-type="action"]:not(.selectedUser) { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; } div.line[data-line-type="privmsg"] .time:not(.selectedUser), div.line[data-line-type="action"] .time:not(.selectedUser) { transition: color 0.5s; } div.line.selectedUser[data-highlight="false"] .time { transition: color 0.5s ease-in; color: #aaa; } div.line.selectedUser[data-highlight="false"] { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; z-index: 190; position: relative; border-top: 1px solid #5E5E5E !important; border-bottom: 1px solid #5E5E5E !important; background-color: rgba(105, 105, 105, 0.3) !important; } /* PRIVMSG */ div.line[data-line-type="privmsg"][data-highlight="false"] { padding: 2px 5px 2px 5px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: none; } div.line[data-line-type="privmsg"][data-highlight="true"], div.line[data-line-type="action"][data-highlight="true"] { z-index: 191; position: relative; font-weight: normal; padding: 2px 5px 2px 5px; border-top: 1px solid #994C00; border-bottom: 1px solid #994C00; background-color: #2d1e10 !important; } div.line[data-line-type="privmsg"] .message { color: #aaa; } div.line[data-line-type="privmsg"][data-member-type="myself"] .message { color: #a9a9a9; } div.line[data-line-type="privmsg"] .sender { font-weight: 700; white-space: pre-wrap; } /* ACTION */ div.line[data-line-type="action"] { padding: 2px 5px 2px 5px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: none; } div.line[data-line-type="action"] .message { color: #aaa; } div.line[data-line-type="action"] .sender { font-weight: 700; } div.line[data-line-type="action"][data-member-type="myself"] .sender, div.line[data-line-type="action"][data-member-type="myself"] .message { color: #a9a9a9; } body[dir="ltr"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-left: 0.4em; */ } /* DEBUG/INVITE */ div.line[data-line-type="invite"], div.line[data-line-type="debug"], div.line[data-line-type="dcc-file-transfer"], div.line[data-line-type="off-the-record-encryption-status"] { color: #777; z-index: 190; background: #222; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #444; border-bottom: 1px solid #444; } /* off-the-record-encryption-status Message Event */ div.line[data-line-type="off-the-record-encryption-status"] .message { color: #ff0000; font-weight: 700; } /* Message of the Day (MOTD) */ /* 720, 721, 722 are used by ShadowIRCd for Oper MOTD. */ /* 372, 375, 376 are normal MOTD shared by several IRCds. */ div.line[data-command="372"], div.line[data-command="721"] { border: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="375"], div.line[data-command="720"] { /* Start. */ border-bottom: none; padding-top: 2px; padding-bottom: 3px; } div.line[data-command="376"], div.line[data-command="722"] { /* End. */ border-top: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="372"] .message, div.line[data-command="375"] .message, div.line[data-command="376"] .message div.line[data-command="720"] .message, div.line[data-command="721"] .message, div.line[data-command="722"] .message { font-family: "Menlo" !important; } /* GENERAL EVENT */ div.line[data-line-type="join"], div.line[data-line-type="part"], div.line[data-line-type="kick"], div.line[data-line-type="quit"], div.line[data-line-type="kill"], div.line[data-line-type="nick"], div.line[data-line-type="mode"], div.line[data-line-type="topic"], div.line[data-line-type="website"] { padding: 3px 5px 3px 5px; color: #3D3D3D; } body[dir="ltr"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="kick"] .message:before, body[dir="ltr"] div.line[data-line-type="part"] .message:before, body[dir="ltr"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="nick"] .message:before { content: "•"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="kick"] .message:before, body[dir="rtl"] div.line[data-line-type="part"] .message:before, body[dir="rtl"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="nick"] .message:before { content: "•"; color: #0c0; /* margin-left: 0.4em; */ } /* Nickname Colors */ .inlineSender { font-weight: 700; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sapientia/inlineMedia.css ================================================ /* Images */ .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin-right: 12px; margin-left: 10px; min-width: 40px; max-width: 90%; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { cursor: pointer; border-radius: 5px; border: 2px solid #a1a1a1; color: #a1a1a1; display: inline-block; line-height: 14px; font-size: 15px; font-family: "Helvetica Neue" !important; text-indent: 7px; width: 16px; height: 16px; float: left; padding-right: 7px; padding-left: 0px; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sapientia/scripts.js ================================================ /* Defined in: "Textual.app -> Contents -> Resources -> JavaScript -> API -> core.js" */ Textual.viewBodyDidLoad = function() { Textual.fadeOutLoadingScreen(1.00, 0.95); } Textual.messageAddedToView = function(line, fromBuffer) { var element = document.getElementById("line-" + line); ConversationTracking.updateNicknameWithNewMessage(element); } Textual.nicknameSingleClicked = function(e) { ConversationTracking.nicknameSingleClickEventCallback(e); } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sapientia/settings.plist ================================================ Appearance dark Channel View Overlay Color #00000066 Force Invert Sidebars Indentation Offset 6 Nickname Color Style HSL-dark Nickname Format %@%n: Template Engine Versions default 4 Underlying Window Color #242323 ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/Varieties/Dark/Templates/encryptedMessageLock.mustache ================================================ [encrypted] ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/Varieties/Dark/design.css ================================================ /* Inline media */ @import "inlineMedia.css"; /* Basic Body Structure */ :root { supported-color-schemes: dark; } * { margin: 0; padding: 0; font-size: 100%; word-wrap: break-word; word-break: break-word; } body { color: #fff; height: 100%; z-index: 100; font-size: 12px; overflow: hidden; background-color: #000; font-family: "Lucida Grande"; } #body { left: 0; right: 0; bottom: 0; width: 100%; max-height: 100%; overflow-y: auto; z-index: 100; position: absolute; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Only factor in height of topic bar when selected == true because that is only time the topic is displayed according to the logic defined by baseLayout.css */ body[data-selected="true"][data-view-type="channel"] #body { max-height: calc(100% - 2.0em); /* height minus topic */ } div.line { margin-top: -1px; clear: both; } body[dir="rtl"] .sender { display: inline-block; } /* Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #393939; box-shadow: inset 1px 0px 0px 0px #4b4b4b; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #7c7c7c; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #b0b0b0; } /* Loading Screen */ #loadingScreen { position: absolute; top: 45%; left: calc(50% - (320px / 2)); /* half of width + padding */ width: 300px; font-size: 18px; background: #181818; border: 1px solid #333333; border-radius: 5px; padding: 5px 10px; opacity: 1; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Time */ body[dir="ltr"] .time { color: #666; white-space: nowrap; } body[dir="rtl"] .time { color: #666; white-space: nowrap; padding-left: 0.4em; display: inline-block; } /* Encryption Lock */ .encryptionLock img { float: right; margin: 0; padding: 0; height: 11px; margin-top: 2px; padding-left: 10px; } /* Links */ a { color: #0080FF; border-color: #0080FF; text-decoration: none; border-bottom: dotted 1px; } a:hover { color: #40A0FF; border-color: #40A0FF; } /* Topic Bar */ #topicBar { top: 0; left: 0; right: 0; opacity: 0; /* Set by JavaScript */ z-index: 400; color: #8E8E8E; position: fixed; padding: 2px 0.5em 3px; box-shadow: 0 1px 5px #777; border-bottom: 1px solid #404040; text-shadow: 1px 1px rgba(10, 10, 10, 0.7); background: -webkit-linear-gradient(top, #0F0F0F 0%, #262626 100%); -webkit-transition: opacity 0.8s linear; -webkit-font-smoothing: subpixel-antialiased; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* Topic bar hover additions contributed with permission from the project: */ #topicBar:hover { overflow: visible; white-space: normal; } #topicBar a, #topicBar span.channel { color: #8E8E8E; border-color: #8E8E8E; text-shadow: 1px 1px rgba(10, 10, 10, 0.7); } /* Remember Line */ #mark { clear: both; position: relative; z-index: 295; margin-top: -1px; border-bottom: 1px dashed; border-color: #444; -webkit-transition: 0.2s linear; } /* Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #5a5a5a; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: 0.5em 0; } .date_indicator > hr, .session_indicator > hr { background: #606060; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; color: #d8d8d8; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* NOTICE/CTCP/WALLOPS */ div.line[data-line-type="ctcp"], div.line[data-line-type="notice"], div.line[data-line-type="wallops"] { color: #f00; z-index: 191; background: #400; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #811; border-bottom: 1px solid #811; } div.line[data-line-type="notice"] .sender { color: #f00; font-weight: 700; } /* Selected User Messages */ .sender { cursor: pointer; } div.line[data-line-type="privmsg"]:not(.selectedUser), div.line[data-line-type="action"]:not(.selectedUser) { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; } div.line[data-line-type="privmsg"] .time:not(.selectedUser), div.line[data-line-type="action"] .time:not(.selectedUser) { transition: color 0.5s; } div.line.selectedUser[data-highlight="false"] .time { transition: color 0.5s ease-in; color: #aaa; } div.line.selectedUser[data-highlight="false"] { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; z-index: 190; position: relative; border-top: 1px solid #02577C !important; border-bottom: 1px solid #02577C !important; background-color: rgba(3, 134, 212, 0.25) !important; } /* PRIVMSG */ div.line[data-line-type="privmsg"][data-highlight="false"] { padding: 2px 5px 2px 5px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: none; } div.line[data-line-type="privmsg"][data-highlight="true"], div.line[data-line-type="action"][data-highlight="true"] { z-index: 191; position: relative; font-weight: normal; padding: 2px 5px 2px 5px; border-top: 1px solid #988C00; border-bottom: 1px solid #988C00; background-color: #362C00 !important; } div.line[data-line-type="privmsg"] .message { color: #aaa; } div.line[data-line-type="privmsg"][data-member-type="myself"] .message { color: #fff; } div.line[data-line-type="privmsg"] .sender { font-weight: 700; white-space: pre-wrap; } /* ACTION */ div.line[data-line-type="action"] { padding: 2px 5px 2px 5px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: none; } div.line[data-line-type="action"] .message { color: #aaa; } div.line[data-line-type="action"] .sender { font-weight: 700; } div.line[data-line-type="action"][data-member-type="myself"] .sender, div.line[data-line-type="action"][data-member-type="myself"] .message { color: #B52CF6; } body[dir="ltr"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-left: 0.4em; */ } /* DEBUG/INVITE */ div.line[data-line-type="invite"], div.line[data-line-type="debug"], div.line[data-line-type="dcc-file-transfer"], div.line[data-line-type="off-the-record-encryption-status"] { color: #777; z-index: 190; background: #222; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #444; border-bottom: 1px solid #444; } /* off-the-record-encryption-status Message Event */ div.line[data-line-type="off-the-record-encryption-status"] .message { color: #ff0000; font-weight: 700; } /* Message of the Day (MOTD) */ /* 720, 721, 722 are used by ShadowIRCd for Oper MOTD. */ /* 372, 375, 376 are normal MOTD shared by several IRCds. */ div.line[data-command="372"], div.line[data-command="721"] { border: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="375"], div.line[data-command="720"] { /* Start. */ border-bottom: none; padding-top: 2px; padding-bottom: 3px; } div.line[data-command="376"], div.line[data-command="722"] { /* End. */ border-top: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="372"] .message, div.line[data-command="375"] .message, div.line[data-command="376"] .message div.line[data-command="720"] .message, div.line[data-command="721"] .message, div.line[data-command="722"] .message { font-family: "Menlo" !important; } /* GENERAL EVENT */ div.line[data-line-type="join"], div.line[data-line-type="part"], div.line[data-line-type="kick"], div.line[data-line-type="quit"], div.line[data-line-type="kill"], div.line[data-line-type="nick"], div.line[data-line-type="mode"], div.line[data-line-type="topic"], div.line[data-line-type="website"] { padding: 3px 5px 3px 5px; color: #666; } body[dir="ltr"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="kick"] .message:before, body[dir="ltr"] div.line[data-line-type="part"] .message:before, body[dir="ltr"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="nick"] .message:before { content:"•"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="kick"] .message:before, body[dir="rtl"] div.line[data-line-type="part"] .message:before, body[dir="rtl"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="nick"] .message:before { content:"•"; color: #0c0; /* margin-left: 0.4em; */ } /* Nickname Colors */ .inlineSender { font-weight: 700; } div.line[data-line-type="privmsg"] .sender[data-member-type="myself"] { color: #B8DFFF; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/Varieties/Dark/inlineMedia.css ================================================ /* Images */ .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin-right: 12px; margin-left: 10px; min-width: 40px; max-width: 90%; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { cursor: pointer; border-radius: 5px; border: 2px solid #a1a1a1; color: #a1a1a1; display: inline-block; line-height: 14px; font-size: 15px; font-family: "Helvetica Neue" !important; text-indent: 7px; width: 16px; height: 16px; float: left; padding-right: 7px; padding-left: 0px; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/Varieties/Dark/settings.plist ================================================ Appearance dark Channel View Overlay Color #00000066 Indentation Offset 6 Nickname Color Style HSL-dark Underlying Window Color #000000 ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/Varieties/Light/Templates/encryptedMessageLock.mustache ================================================ [encrypted] ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/Varieties/Light/design.css ================================================ /* Inline media */ @import "inlineMedia.css"; /* Basic Body Structure */ :root { supported-color-schemes: light; } * { margin: 0; padding: 0; font-size: 100%; word-wrap: break-word; word-break: break-word; } body { color: #000; height: 100%; z-index: 100; font-size: 12px; overflow: hidden; background-color: #fff; font-family: "Lucida Grande"; } #body { left: 0; right: 0; bottom: 0; width: 100%; max-height: 100%; overflow-y: auto; z-index: 100; position: absolute; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Only factor in height of topic bar when selected == true because that is only time the topic is displayed according to the logic defined by baseLayout.css */ body[data-selected="true"][data-view-type="channel"] #body { max-height: calc(100% - 2.0em); /* height minus topic */ } div.line { margin-top: -1px; clear: both; } body[dir="rtl"] .sender { display: inline-block; } /* Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #fbfbfb; box-shadow: inset 1px 0px 0px 0px #e8e8e8; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #c1c1c1; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #7d7d7d; } /* Loading Screen */ #loadingScreen { position: absolute; top: 45%; left: calc(50% - (320px / 2)); /* half of width + padding */ width: 300px; font-size: 18px; background: #f3f3f3; border: 1px solid #d7d7d7; border-radius: 5px; padding: 5px 10px; opacity: 1; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Time */ body[dir="ltr"] .time { color: #aaa; white-space: nowrap; } body[dir="rtl"] .time { color: #aaa; white-space: nowrap; padding-left: 0.4em; display: inline-block; } /* Encryption Lock */ .encryptionLock img { float: right; margin: 0; padding: 0; height: 11px; margin-top: 2px; padding-left: 10px; } /* Links */ a { color: #00e; border-color: #00e; text-decoration: none; border-bottom: dotted 1px; } a:hover { color: #0080FF; border-color: #0080FF; } /* Topic Bar */ #topicBar { top: 0; left: 0; right: 0; z-index: 400; opacity: 0; /* Set by JavaScript */ color: #FFFFFF; position: fixed; padding: 2px 0.5em 3px; box-shadow: 0 1px 5px #777; border-bottom: 1px solid #61778F; text-shadow: 1px 1px rgba(83, 86, 94, 0.7); background: -webkit-linear-gradient(top, #A2B0D0 0%, #7385AD 100%); -webkit-transition: opacity 0.8s linear; -webkit-font-smoothing: subpixel-antialiased; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* Topic bar hover additions contributed with permission from the project: */ #topicBar:hover { overflow: visible; white-space: normal; } #topicBar a, #topicBar span.channel { color: #FFFFFF; border-color: #FFFFFF; text-shadow: 1px 1px rgba(83, 86, 94, 0.7); } /* Remember Line */ #mark { clear: both; position: relative; z-index: 295; margin-top: -1px; border-bottom: 1px dashed; border-color: #444; -webkit-transition: 0.2s linear; } /* Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #b6b6b6; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: 0.5em 0; } .date_indicator > hr, .session_indicator > hr { background: #dbdbdb; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; color: #a6a6a6; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* NOTICE/CTCP/WALLOPS */ div.line[data-line-type="ctcp"], div.line[data-line-type="notice"], div.line[data-line-type="wallops"] { color: #f00; z-index: 191; background: #fcc; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #f77; border-bottom: 1px solid #f77; } div.line[data-line-type="notice"] .sender { color: #f00; font-weight: 700; } /* Selected User Messages */ .sender { cursor: pointer; } div.line[data-line-type="privmsg"]:not(.selectedUser), div.line[data-line-type="action"]:not(.selectedUser) { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; } div.line[data-line-type="privmsg"] .time:not(.selectedUser), div.line[data-line-type="action"] .time:not(.selectedUser) { transition: color 0.5s; } div.line.selectedUser[data-highlight="false"] .time { transition: color 0.5s ease-in; color: #000; } div.line.selectedUser[data-highlight="false"] { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; z-index: 190; position: relative; border-top: 1px solid #FC5AF8 !important; border-bottom: 1px solid #FC5AF8 !important; background-color: rgba(246, 70, 255, 0.2) !important; } /* PRIVMSG */ div.line[data-line-type="privmsg"][data-highlight="false"] { padding: 2px 5px 2px 5px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: none; } div.line[data-line-type="privmsg"][data-highlight="true"], div.line[data-line-type="action"][data-highlight="true"] { z-index: 191; position: relative; font-weight: normal; padding: 2px 5px 2px 5px; border-top: 1px solid #F6E73F; border-bottom: 1px solid #F6E73F; background-color: #FAF4AF !important; } div.line[data-line-type="privmsg"] .message { color: #000; } div.line[data-line-type="privmsg"][data-member-type="myself"] .message { color: #000; } div.line[data-line-type="privmsg"] .sender { font-weight: 700; white-space: pre-wrap; } /* ACTION */ div.line[data-line-type="action"] { padding: 2px 5px 2px 5px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; background-color: none; } div.line[data-line-type="action"] .message { color: #000; } div.line[data-line-type="action"] .sender { font-weight: 700; } div.line[data-line-type="action"][data-member-type="myself"] .sender, div.line[data-line-type="action"][data-member-type="myself"] .message { color: #B52CF6; } body[dir="ltr"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-left: 0.4em; */ } /* DEBUG/INVITE */ div.line[data-line-type="invite"], div.line[data-line-type="debug"], div.line[data-line-type="dcc-file-transfer"], div.line[data-line-type="off-the-record-encryption-status"] { color: #777; z-index: 190; background: #eee; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; } /* off-the-record-encryption-status Message Event */ div.line[data-line-type="off-the-record-encryption-status"] .message { color: #ff0000; font-weight: 700; } /* Message of the Day (MOTD) */ /* 720, 721, 722 are used by ShadowIRCd for Oper MOTD. */ /* 372, 375, 376 are normal MOTD shared by several IRCds. */ div.line[data-command="372"], div.line[data-command="721"] { border: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="375"], div.line[data-command="720"] { /* Start. */ border-bottom: none; padding-top: 2px; padding-bottom: 3px; } div.line[data-command="376"], div.line[data-command="722"] { /* End. */ border-top: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="372"] .message, div.line[data-command="375"] .message, div.line[data-command="376"] .message div.line[data-command="720"] .message, div.line[data-command="721"] .message, div.line[data-command="722"] .message { font-family: "Menlo" !important; } /* GENERAL EVENT */ div.line[data-line-type="join"], div.line[data-line-type="part"], div.line[data-line-type="kick"], div.line[data-line-type="quit"], div.line[data-line-type="kill"], div.line[data-line-type="nick"], div.line[data-line-type="mode"], div.line[data-line-type="topic"], div.line[data-line-type="website"] { padding: 3px 5px 3px 5px; color: #aaa; } body[dir="ltr"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="kick"] .message:before, body[dir="ltr"] div.line[data-line-type="part"] .message:before, body[dir="ltr"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="nick"] .message:before { content:"•"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="kick"] .message:before, body[dir="rtl"] div.line[data-line-type="part"] .message:before, body[dir="rtl"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="nick"] .message:before { content:"•"; color: #0c0; /* margin-left: 0.4em; */ } /* Nickname Colors */ .inlineSender { font-weight: 700; } div.line[data-line-type="privmsg"] .sender[data-member-type="myself"] { color: #ea0d68; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/Varieties/Light/inlineMedia.css ================================================ /* Images */ .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin-right: 12px; margin-left: 10px; min-width: 40px; max-width: 90%; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { cursor: pointer; border-radius: 5px; border: 2px solid #a1a1a1; color: #a1a1a1; display: inline-block; line-height: 14px; font-size: 15px; font-family: "Helvetica Neue" !important; text-indent: 7px; width: 16px; height: 16px; float: left; padding-right: 7px; padding-left: 0px; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/Varieties/Light/settings.plist ================================================ Appearance light Channel View Overlay Color #00000033 Indentation Offset 6 Nickname Color Style HSL-light Underlying Window Color #FFFFFF ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/copyright.txt ================================================ This style is a modified and stripped down version of the "Simplified" theme developed by "Cowboy" Ben Alman (http://benalman.com/). Copyright © 2010, 2011, 2012. ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/scripts.js ================================================ /* Defined in: "Textual.app -> Contents -> Resources -> JavaScript -> API -> core.js" */ Textual.viewBodyDidLoad = function() { Textual.fadeOutLoadingScreen(1.00, 0.95); } Textual.messageAddedToView = function(line, fromBuffer) { var element = document.getElementById("line-" + line); ConversationTracking.updateNicknameWithNewMessage(element); } Textual.nicknameSingleClicked = function(e) { ConversationTracking.nicknameSingleClickEventCallback(e); } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Simplified/settings.plist ================================================ Template Engine Versions default 4 ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sulaco/LICENSE.txt ================================================ Copyright (c) 2013 Ryan Grove (ryan@wonko.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sulaco/Templates/newMessagePostedWithSender.mustache ================================================
{{#isEncrypted}} {{{encryptedMessageLockTemplate}}} {{/isEncrypted}} {{formattedNickname}} {{{formattedMessage}}} {{#inlineMediaEnabled}} {{/inlineMediaEnabled}} {{formattedTimestamp}}
================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sulaco/Templates/newMessagePostedWithoutSender.mustache ================================================
{{{formattedMessage}}} {{formattedTimestamp}}
================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sulaco/design.css ================================================ /* Inline media */ @import "inlineMedia.css"; /* Basic Structure */ :root { supported-color-schemes: dark; } body { background: #222; color: #cfcfcf; font-family: 'Myriad Pro'; font-size: 16px; line-height: 1.4; margin: 0; overflow: hidden; word-wrap: break-word; word-break: break-word; } a { border-bottom: dotted 1px #8cb3ff; color: #8cb3ff; text-decoration: none; } a:hover { border: none; border-bottom: solid 1px #8cb3ff; } #body { left: 0; right: 0; bottom: 0; width: 100%; max-height: 100%; overflow-y: auto; z-index: 100; position: absolute; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } .message, .sender, .time { padding: 4px 8px 3px; } .message { width: 100%; } .sender, .time { flex-shrink: 0; background: #333; } .sender { font-weight: 600; overflow: hidden; text-align: right; text-overflow: ellipsis; white-space: nowrap; width: 10em; } body[data-view-type="sender"] .sender { display: none; } .time { color: #888; font-size: 0.8em; line-height: 1.8; white-space: nowrap; } div.line { display: flex; display: -webkit-flex; flex-flow: row; -webkit-flex-flow: row; } div.line[data-member-type="myself"] .message { background: #2e2e2e; } div.line[data-line-type="action"] .message, div.line[data-line-type="action"] .sender { background: none; font-style: italic; } div.line[data-line-type="action"] .sender { padding-right: 2px; } body[data-view-type="channel"] .event .message { color: #666; } .event .message:before { content: '» '; } body[data-view-type="server"] .event .message { color: #999; font-size: inherit; padding-left: 1em; padding-right: 2em; } body[data-view-type="server"] .event .message:before { content: ''; } .event .sender { background: inherit; } div.line.text:hover, body[data-view-type="server"] .line:hover { outline: 1px solid #666; } div.line.text:hover .message, div.line.text:hover .sender, div.line.text:hover .time, body[data-view-type="server"] .line:hover .message, body[data-view-type="server"] .line:hover .time { background-color: #444; color: #fff; } div.line.text[data-highlight="true"], div.line.text[data-highlight="true"] .time { color: #fff; } div.line.text[data-highlight="true"] { border: 1px solid #284e84; border-left: none; border-right: none; outline: none; } div.line.text[data-highlight="true"]:hover { border-color: #3069ab; } div.line.text[data-highlight="true"] + div.line.text[data-highlight="true"] { border-top: none; } div.line.text[data-highlight="true"] .message, div.line.text[data-highlight="true"] .sender, div.line.text[data-highlight="true"] .time { background-color: #1b3458; } div.line.text[data-highlight="true"]:hover .message, div.line.text[data-highlight="true"]:hover .sender, div.line.text[data-highlight="true"]:hover .time { background-color: #204571; } /* Selected User */ .sender { cursor: pointer; } div.line[data-line-type="privmsg"]:not(.selectedUser), div.line[data-line-type="action"]:not(.selectedUser) { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; } div.line[data-line-type="privmsg"] .time:not(.selectedUser), div.line[data-line-type="action"] .time:not(.selectedUser) { transition: color 0.5s; } div.line.selectedUser[data-highlight="false"] .time { transition: color 0.5s ease-in; color: #fff; } div.line.selectedUser[data-highlight="false"] { transition-property: color, border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s, 0.5s; color: #fff; border: 1px solid #942f25; border-left: none; border-right: none; outline: none; } div.line.selectedUser[data-highlight="false"]:hover { border-color: #aa362b; } div.line.selectedUser[data-highlight="false"] + div.line.selectedUser[data-highlight="false"] { border-top: none; } div.line.selectedUser[data-highlight="false"] .message, div.line.selectedUser[data-highlight="false"] .sender, div.line.selectedUser[data-highlight="false"] .time { background-color: #521e17; } div.line.selectedUser[data-highlight="false"]:hover .message, div.line.selectedUser[data-highlight="false"]:hover .sender, div.line.selectedUser[data-highlight="false"]:hover .time { background-color: #68271d; } /* Remember Line */ #mark { border-bottom: 1px dotted #9f9f9f; margin: 1px 0; -webkit-transition: 0.2s linear; } /* Loading Screen */ #loadingScreen { position: absolute; top: 45%; left: calc(50% - (320px / 2)); /* half of width + padding */ width: 300px; font-size: 18px; background: #181818; border: 1px solid #333; border-radius: 5px; padding: 5px 10px; opacity: 1; -webkit-transition: opacity 0.8s linear; } /* Topic Bar */ #topicBar { top: 0; left: 0; right: 0; z-index: 400; opacity: 0; /* Set by JavaScript */ color: #efefef; position: fixed; padding: 4px 12px 2px; box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.8); border-bottom: 1px solid #404040; text-shadow: 1px 1px rgba(83, 86, 94, 0.7); background: #333; font-weight: 600; text-align: center; -webkit-transition: opacity 0.8s linear; -webkit-font-smoothing: subpixel-antialiased; } #topicBar a, #topicBar span.channel { color: #f5f5f5; border-color: #f5f5f5; text-shadow: 1px 1px rgba(10, 10, 10, 0.7); } /* Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #393939; box-shadow: inset 1px 0px 0px 0px #4b4b4b; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #7c7c7c; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #b0b0b0; } /* Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #797979; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: 0.5em 0; } .date_indicator > hr, .session_indicator > hr { background: #444; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; color: #919191; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* Nickname Colors */ data.line.text .sender[data-member-type="myself"] { color: #ff8c00; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sulaco/inlineMedia.css ================================================ /* Images */ .inlineImage, .inlineVideo, .inlineVideoService { margin-top: 8px !important; margin-bottom: 8px !important; } .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin-right: 15px; margin-left: 10px; min-width: 40px; max-width: 90%; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { cursor: pointer; border-radius: 5px; border: 2px solid #a1a1a1; color: #a1a1a1; display: inline-block; line-height: 14px; font-size: 15px; font-family: "Helvetica Neue" !important; text-indent: 7px; width: 16px; height: 16px; float: left; padding-right: 7px; padding-left: 0px; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sulaco/scripts.js ================================================ // -- Sulaco ------------------------------------------------------------------- var Sulaco; Sulaco = { coalesceMessages: function (line) { var previousLine = Sulaco.getPreviousLine(line); var previousSender = Sulaco.getSenderNickname(previousLine); var sender = Sulaco.getSenderNickname(line); if (sender === null || previousSender === null) { return; } if (sender === previousSender && Sulaco.getLineType(line) === 'privmsg' && Sulaco.getLineType(previousLine) === 'privmsg') { line.classList.add('coalesced'); Sulaco.getSenderElement(line).innerHTML = ''; } }, getPreviousLine: function (line) { var previousLine = line.previousElementSibling; if (previousLine && previousLine.classList && previousLine.classList.contains('line')) { return previousLine; } return null; }, getLineType: function (line) { return ((line) ? line.dataset.lineType : null); }, getMessage: function (line) { return ((line) ? line.querySelector('.message').textContent.trim() : null); }, getSenderElement: function (line) { return ((line) ? line.querySelector('.sender') : null); }, getSenderNickname: function (line) { var sender = Sulaco.getSenderElement(line); return ((sender) ? sender.dataset.nickname : null); } }; // -- Textual ------------------------------------------------------------------ /* Defined in: "Textual.app -> Contents -> Resources -> JavaScript -> API -> core.js" */ Textual.viewBodyDidLoad = function() { Textual.fadeOutLoadingScreen(1.00, 0.90); } Textual.messageAddedToView = function(line, fromBuffer) { var element = document.getElementById("line-" + line); Sulaco.coalesceMessages(element); ConversationTracking.updateNicknameWithNewMessage(element); } Textual.nicknameSingleClicked = function(e) { ConversationTracking.nicknameSingleClickEventCallback(e); } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Sulaco/settings.plist ================================================ Appearance dark Channel View Overlay Color #00000066 Indentation Offset 6 Nickname Color Style HSL-dark Nickname Format %@%n Template Engine Versions default 4 Underlying Window Color #222222 ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/Varieties/Dark/Templates/encryptedMessageLock.mustache ================================================ [encrypted] ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/Varieties/Dark/design.css ================================================ /* Colors taken from the Tomorrow and base16 themes: * https://github.com/ChrisKempson/Tomorrow-Theme * https://github.com/chriskempson/base16 */ /* @group Inline media */ @import "inlineMedia.css"; /* @end */ /* @group Basic Body Structure */ :root { supported-color-schemes: dark; } * { margin: 0; padding: 0; font-size: 100%; word-wrap: break-word; word-break: break-word; } body { color: #CCCCCC; /* Tomorrow Night Eighties: Foreground */ height: 100%; z-index: 100; font-size: 12px; overflow: hidden; background-color: #2D2D2D; /* Tomorrow Night Eighties: Background */ font-family: "EspressoMono-Regular", "Menlo"; } #body { left: 0; right: 0; bottom: 0; width: 100%; max-height: 100%; overflow-y: auto; z-index: 100; position: absolute; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Only factor in height of topic bar when selected == true because that is only time the topic is displayed according to the logic defined by baseLayout.css */ body[data-selected="true"][data-view-type="channel"] #body { max-height: calc(100% - 1.7em); /* height minus topic */ } div.line { padding: 2px 5px 2px 5px; clear: both; } .sender { cursor: pointer; font-weight: 700; } body[dir="rtl"] .sender { display: inline-block; } /* @end */ /* @group Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #393939; box-shadow: inset 1px 0px 0px 0px #4b4b4b; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #7c7c7c; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #b0b0b0; } /* @end */ /* @group Misc */ #loadingScreen { position: absolute; top: 45%; left: calc(50% - (320px / 2)); /* half of width + padding */ width: 300px; font-size: 18px; background: #393939; /* Tomorrow Night Eighties: Current Line */ border: 1px solid #373B41; /* Tomorrow: Selection */ border-radius: 5px; padding: 5px 10px; opacity: 1; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; text-align: center; } .encryptionLock img { float: right; margin: 0; padding: 0; height: 11px; margin-top: 2px; padding-left: 10px; } /* @end */ /* @group Time */ .time { color: #515151; /* Tomorrow Night Eighties: Selection */ white-space: nowrap; } body[dir="rtl"] .time { padding-left: 0.4em; display: inline-block; } /* @end */ /* @group Links */ a { color: #6699CC; /* Tomorrow Night Eighties: Blue */ border-color: #6699CC; /* Tomorrow Night Eighties: Blue */ text-decoration: none; border-bottom: dotted 1px; } a:hover { color: #CC99CC; /* Tomorrow Night Eighties: Purple */ border-color: #CC99CC; /* Tomorrow Night Eighties: Purple */ } /* @end */ /* @group Topic Bar */ #topicBar { top: 0; left: 0; right: 0; z-index: 400; opacity: 0; /* Set by JavaScript */ color: #D1F1A9; /* Tomorrow Night Blue: Green */ position: fixed; padding: 2px 0.5em 3px; background-color: rgba(113, 140, 00, 0.9); /* Tomorrow: Green 90% */ -webkit-transition: opacity 0.8s linear; -webkit-font-smoothing: subpixel-antialiased; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* Topic bar hover additions contributed with permission from the project: * https://github.com/hbang/Simplified-Light-Modifications */ #topicBar:hover { overflow: visible; white-space: normal; } /* @end */ /* @group Separating History */ #mark { clear: both; position: relative; z-index: 295; margin-top: -1px; border-bottom: 1px dashed; border-color: #F2777A; /* Tomorrow Night Eighties: Red */ -webkit-transition: 0.2s linear; } /* @end */ /* @group Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #797979; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* @end */ /* @group Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: 0.5em 0; } .date_indicator > hr, .session_indicator > hr { background: #444; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; color: #919191; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* @end */ /* @group NOTICE / CTCP / WALLOPS */ div.line[data-line-type="ctcp"], div.line[data-line-type="notice"], div.line[data-line-type="wallops"] { color: #FFCC66; /* Tomorrow Night Eighties: Yellow */ z-index: 191; background-color: rgba(234, 183, 00, 0.2); /* Tomorrow: Yellow 20% */ position: relative; } /* @end */ /* @group Selected User */ div.line[data-line-type="privmsg"]:not(.selectedUser), div.line[data-line-type="action"]:not(.selectedUser) { transition: background-color 0.5s; } div.line.selectedUser[data-highlight="false"] { transition: background-color 0.5s; z-index: 190; position: relative; background-color: rgba(245, 135, 31, 0.2) !important; /* Tomorrow: Orange 20% */ } /* @end */ /* @group PRIVMSG */ div.line[data-line-type="privmsg"][data-highlight="true"], div.line[data-line-type="action"][data-highlight="true"] { z-index: 191; position: relative; font-weight: normal; background-color: rgba(200, 40, 41, 0.2) !important; /* Tomorrow: Red 20% */ } div.line[data-line-type="privmsg"] .sender { white-space: pre-wrap; } /* @end */ /* @group ACTION */ div.line[data-line-type="action"] .sender:before { content: "•"; } body[dir="ltr"] div.line[data-line-type="action"] .sender:before { margin-right: 0.4em; } body[dir="rtl"] div.line[data-line-type="action"] .sender:before { margin-left: 0.4em; } div.line[data-line-type="action"] .sender:after { content: ""; } /* @end */ /* @group DEBUG / INVITE */ div.line[data-line-type="invite"], div.line[data-line-type="debug"], div.line[data-line-type="dcc-file-transfer"], div.line[data-line-type="off-the-record-encryption-status"] { color: #4D4D4C; /* Tomorrow: Foreground */ z-index: 190; background: rgba(55, 59, 65, 0.2); /* Tomorrow Night: Selection 20% */ position: relative; } /* off-the-record-encryption-status Message Event */ div.line[data-line-type="off-the-record-encryption-status"] .message { color: #F2777A; font-weight: 700; } /* @end */ /* @group Message of the Day (MOTD) * * 720, 721, 722 are used by ShadowIRCd for Oper MOTD. * 372, 375, 376 are normal MOTD shared by several IRCds. */ div.line[data-command="372"], div.line[data-command="721"] { padding-top: 3px; padding-bottom: 3px; } div.line[data-command="375"], div.line[data-command="720"] { /* Start. */ padding-top: 2px; padding-bottom: 3px; } div.line[data-command="376"], div.line[data-command="722"] { /* End. */ padding-top: 3px; padding-bottom: 3px; } div.line[data-command="372"] .message, div.line[data-command="375"] .message, div.line[data-command="376"] .message div.line[data-command="720"] .message, div.line[data-command="721"] .message, div.line[data-command="722"] .message { font-family: "EspressoMono-Regular", "Menlo" !important; } /* @end */ /* @group General Events */ div.line[data-line-type="join"], div.line[data-line-type="part"], div.line[data-line-type="quit"], div.line[data-line-type="nick"], div.line[data-line-type="mode"], div.line[data-line-type="topic"], div.line[data-line-type="website"] { color: #424242; /* Tomorrow Night Bright: Selection */ } /* Slightly more interesting events */ div.line[data-line-type="kick"], div.line[data-line-type="kill"], div.line[data-line-type="mode"][data-command="mode"] { color: #515151; /* Tomorrow Night Eighties: Selection */ } /* @group Event Indicators */ div.line[data-line-type="join"] .message:before { content: "→"; color: #99CC99; /* Tomorrow Night Eighties: Green */ } div.line[data-line-type="kick"] .message:before, div.line[data-line-type="part"] .message:before, div.line[data-line-type="quit"] .message:before { content: "←"; color: #F2777A; /* Tomorrow Night Eighties: Red */ } div.line[data-line-type="nick"] .message:before { content:"◦"; color: #66CCCC; /* Tomorrow Night Eighties: Aqua */ } /* @end */ /* @end */ /* @group Own Messages */ .sender[data-member-type="myself"] { color: #6699CC; /* Tomorrow Night Eighties: Blue */ } div[data-member-type="myself"] { background-color: rgba(66, 113, 174, 0.2); /* Tomorrow: Blue 20% */ } /* @end */ /* @group mIRC Color Codes */ /* @group Foreground Colors */ .effect[data-foreground-color="0"] { color: #EAEAEA !important; /* Tomorrow Night Bright: Foreground */ } .effect[data-foreground-color="1"] { color: #515151 !important; /* Tomorrow Night Eighties: Selection */ } .effect[data-foreground-color="2"] { color: #6699CC !important; /* Tomorrow Night Eighties: Blue */ } .effect[data-foreground-color="3"] { color: #99CC99 !important; /* Tomorrow Night Eighties: Green */ } .effect[data-foreground-color="4"] { color: #F2777A !important; /* Tomorrow Night Eighties: Red */ } .effect[data-foreground-color="5"] { color: #FF9DA4 !important; /* Tomorrow Night Blue: Red */ } .effect[data-foreground-color="6"] { color: #CC99CC !important; /* Tomorrow Night Eighties: Purple */ } .effect[data-foreground-color="7"] { color: #F99157 !important; /* Tomorrow Night Eighties: Orange */ } .effect[data-foreground-color="8"] { color: #FFCC66 !important; /* Tomorrow Night Eighties: Yellow */ } .effect[data-foreground-color="9"] { color: #B9CA4A !important; /* Tomorrow Night Bright: Green */ } .effect[data-foreground-color="10"] { color: #66CCCC !important; /* Tomorrow Night Eighties: Aqua */ } .effect[data-foreground-color="11"] { color: #99FFFF !important; /* Tomorrow Night Blue: Aqua */ } .effect[data-foreground-color="12"] { color: #7AA6DA !important; /* Tomorrow Night Bright: Blue */ } .effect[data-foreground-color="13"] { color: #C397D8 !important; /* Tomorrow Night Bright: Purple */ } .effect[data-foreground-color="14"] { color: #999999 !important; /* Tomorrow Night Eighties: Comment */ } .effect[data-foreground-color="15"] { color: #CCCCCC !important; /* Tomorrow Night Eighties: Foreground */ } /* @end */ /* @group Background Colors */ .effect[data-background-color="0"] { background-color: #EAEAEA !important; /* Tomorrow Night Bright: Foreground */ } .effect[data-background-color="1"] { background-color: #515151 !important; /* Tomorrow Night Eighties: Selection */ } .effect[data-background-color="2"] { background-color: #6699CC !important; /* Tomorrow Night Eighties: Blue */ } .effect[data-background-color="3"] { background-color: #99CC99 !important; /* Tomorrow Night Eighties: Green */ } .effect[data-background-color="4"] { background-color: #F2777A !important; /* Tomorrow Night Eighties: Red */ } .effect[data-background-color="5"] { background-color: #FF9DA4 !important; /* Tomorrow Night Blue: Red */ } .effect[data-background-color="6"] { background-color: #CC99CC !important; /* Tomorrow Night Eighties: Purple */ } .effect[data-background-color="7"] { background-color: #F99157 !important; /* Tomorrow Night Eighties: Orange */ } .effect[data-background-color="8"] { background-color: #FFCC66 !important; /* Tomorrow Night Eighties: Yellow */ } .effect[data-background-color="9"] { background-color: #B9CA4A !important; /* Tomorrow Night Bright: Green */ } .effect[data-background-color="10"] { background-color: #66CCCC !important; /* Tomorrow Night Eighties: Aqua */ } .effect[data-background-color="11"] { background-color: #99FFFF !important; /* Tomorrow Night Blue: Aqua */ } .effect[data-background-color="12"] { background-color: #7AA6DA !important; /* Tomorrow Night Bright: Blue */ } .effect[data-background-color="13"] { background-color: #C397D8 !important; /* Tomorrow Night Bright: Purple */ } .effect[data-background-color="14"] { background-color: #999999 !important; /* Tomorrow Night Eighties: Comment */ } .effect[data-background-color="15"] { background-color: #CCCCCC !important; /* Tomorrow Night Eighties: Foreground */ } /* @end */ /* @end */ /* vim: set foldmethod=marker: vim: set foldmarker=@group,@end: vim: set nofoldenable: */ ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/Varieties/Dark/inlineMedia.css ================================================ /* @group Images */ .inlineImage, .inlineVideo, .inlineVideoService { margin-top: 8px !important; margin-bottom: 8px !important; } .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin-right: 15px; margin-left: 10px; min-width: 40px; max-width: 90%; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { cursor: pointer; border-radius: 5px; border: 2px solid #a1a1a1; color: #a1a1a1; display: inline-block; line-height: 14px; font-size: 15px; font-family: "Helvetica Neue" !important; text-indent: 7px; width: 16px; height: 16px; float: left; padding-right: 7px; padding-left: 0px; } /* @end */ ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/Varieties/Dark/settings.plist ================================================ Appearance dark Channel View Overlay Color #00000066 Indentation Offset 6 Nickname Color Style HSL-dark Underlying Window Color #2D2D2D ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/Varieties/Light/Templates/encryptedMessageLock.mustache ================================================ [encrypted] ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/Varieties/Light/design.css ================================================ /* Colors taken from the Tomorrow and base16 themes: * https://github.com/ChrisKempson/Tomorrow-Theme * https://github.com/chriskempson/base16 */ /* @group Inline media */ @import "inlineMedia.css"; /* @end */ /* @group Basic Body Structure */ :root { supported-color-schemes: light; } * { margin: 0; padding: 0; font-size: 100%; word-wrap: break-word; word-break: break-word; } body { color: #4D4D4C; /* Tomorrow: Foreground */ height: 100%; z-index: 100; font-size: 12px; overflow: hidden; background-color: #FFFFFF; /* Tomorrow: Background */ font-family: "EspressoMono-Regular", "Menlo"; } #body { left: 0; right: 0; bottom: 0; width: 100%; max-height: 100%; overflow-y: auto; z-index: 100; position: absolute; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Only factor in height of topic bar when selected == true because that is only time the topic is displayed according to the logic defined by baseLayout.css */ body[data-selected="true"][data-view-type="channel"] #body { max-height: calc(100% - 1.7em); /* height minus topic */ } div.line { padding: 2px 5px 2px 5px; clear: both; } .sender { cursor: pointer; font-weight: 700; } body[dir="rtl"] .sender { display: inline-block; } /* @end */ /* @group Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #fbfbfb; box-shadow: inset 1px 0px 0px 0px #e8e8e8; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #c1c1c1; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #7d7d7d; } /* @end */ /* @group Misc */ #loadingScreen { position: absolute; top: 45%; left: calc(50% - (320px / 2)); /* half of width + padding */ width: 300px; font-size: 18px; background: #EFEFEF; /* Tomorrow: Current Line */ border: 1px solid #D6D6D6; /* Tomorrow: Selection */ border-radius: 5px; padding: 5px 10px; opacity: 1; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; text-align: center; } .encryptionLock img { float: right; margin: 0; padding: 0; height: 11px; margin-top: 2px; padding-left: 10px; } /* @end */ /* @group Time */ .time { color: #C5C8C6; /* Tomorrow Night: Foreground */ white-space: nowrap; } body[dir="rtl"] .time { padding-left: 0.4em; display: inline-block; } /* @end */ /* @group Links */ a { color: #4271AE; /* Tomorrow: Blue */ border-color: #4271AE; /* Tomorrow: Blue */ text-decoration: none; border-bottom: dotted 1px; } a:hover { color: #8959A8; /* Tomorrow: Purple */ border-color: #8959A8; /* Tomorrow: Purple */ } /* @end */ /* @group Topic Bar */ #topicBar { top: 0; left: 0; right: 0; z-index: 400; opacity: 0; /* Set by JavaScript */ color: #718C00; /* Tomorrow Night Blue: Green */ position: fixed; padding: 2px 0.5em 3px; background-color: rgba(209, 241, 169, 0.95); /* Tomorrow Night Blue: Green 95% */ -webkit-transition: opacity 0.8s linear; -webkit-font-smoothing: subpixel-antialiased; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* Topic bar hover additions contributed with permission from the project: * https://github.com/hbang/Simplified-Light-Modifications */ #topicBar:hover { overflow: visible; white-space: normal; } /* @end */ /* @group Separating History */ #mark { clear: both; position: relative; z-index: 295; margin-top: -1px; border-bottom: 1px dashed; border-color: #C82829; /* Tomorrow: Red */ -webkit-transition: 0.2s linear; } /* @end */ /* @group Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #b6b6b6; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* @end */ /* @group Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: 0.5em 0; } .date_indicator > hr, .session_indicator > hr { background: #dbdbdb; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; color: #a6a6a6; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* @end */ /* @group NOTICE / CTCP / WALLOPS */ div.line[data-line-type="ctcp"], div.line[data-line-type="notice"], div.line[data-line-type="wallops"] { color: #EAB700; /* Tomorrow: Yellow */ z-index: 191; background-color: rgba(255, 238, 173, 0.25); /* Tomorrow Night Blue: Yellow 25% */ position: relative; } /* @end */ /* @group Selected User */ div.line[data-line-type="privmsg"]:not(.selectedUser), div.line[data-line-type="action"]:not(.selectedUser) { transition: background-color 0.5s; } div.line.selectedUser[data-highlight="false"] { transition: background-color 0.5s; z-index: 190; position: relative; background-color: rgba(255, 197, 143, 0.25) !important; /* Tomorrow Night Blue: Orange 25% */ } /* @end */ /* @group PRIVMSG */ div.line[data-line-type="privmsg"][data-highlight="true"], div.line[data-line-type="action"][data-highlight="true"] { z-index: 191; position: relative; font-weight: normal; background-color: rgba(255, 157, 164, 0.25) !important; /* Tomorrow Night Blue: Red 25% */ } div.line[data-line-type="privmsg"] .sender { white-space: pre-wrap; } /* @end */ /* @group ACTION */ div.line[data-line-type="action"] .sender:before { content: "•"; } body[dir="ltr"] div.line[data-line-type="action"] .sender:before { margin-right: 0.4em; } body[dir="rtl"] div.line[data-line-type="action"] .sender:before { margin-left: 0.4em; } div.line[data-line-type="action"] .sender:after { content: ""; } /* @end */ /* @group DEBUG / INVITE */ div.line[data-line-type="invite"], div.line[data-line-type="debug"], div.line[data-line-type="dcc-file-transfer"], div.line[data-line-type="off-the-record-encryption-status"] { color: #C5C8C6; /* Tomorrow Night: Foreground */ z-index: 190; background: rgba(214, 214, 214, 0.25); /* Tomorrow: Selection 25% */ position: relative; } /* off-the-record-encryption-status Message Event */ div.line[data-line-type="off-the-record-encryption-status"] .message { color: #C82829; font-weight: 700; } /* @end */ /* @group Message of the Day (MOTD) * * 720, 721, 722 are used by ShadowIRCd for Oper MOTD. * 372, 375, 376 are normal MOTD shared by several IRCds. */ div.line[data-command="372"], div.line[data-command="721"] { padding-top: 3px; padding-bottom: 3px; } div.line[data-command="375"], div.line[data-command="720"] { /* Start. */ padding-top: 2px; padding-bottom: 3px; } div.line[data-command="376"], div.line[data-command="722"] { /* End. */ padding-top: 3px; padding-bottom: 3px; } div.line[data-command="372"] .message, div.line[data-command="375"] .message, div.line[data-command="376"] .message div.line[data-command="720"] .message, div.line[data-command="721"] .message, div.line[data-command="722"] .message { font-family: "EspressoMono-Regular", "Menlo" !important; } /* @end */ /* @group General Events */ div.line[data-line-type="join"], div.line[data-line-type="part"], div.line[data-line-type="quit"], div.line[data-line-type="nick"], div.line[data-line-type="mode"], div.line[data-line-type="topic"], div.line[data-line-type="website"] { color: #C5C8C6; /* Tomorrow Night: Foreground */ } /* Slightly more interesting events */ div.line[data-line-type="kick"], div.line[data-line-type="kill"], div.line[data-line-type="mode"][data-command=mode] { color: #8E908C; /* Tomorrow: Comment */ } /* @group Event Indicators */ div.line[data-line-type="join"] .message:before { content: "→"; color: #718C00; /* Tomorrow: Green */ } div.line[data-line-type="kick"] .message:before, div.line[data-line-type="part"] .message:before, div.line[data-line-type="quit"] .message:before { content: "←"; color: #C82829; /* Tomorrow: Red */ } div.line[data-line-type="nick"] .message:before { content:"◦"; color: #3E999F; /* Tomorrow: Aqua */ } /* @end */ /* @end */ /* @group Own Messages */ .sender[data-member-type="myself"] { color: #4271AE; /* Tomorrow: Blue */ } div[data-member-type="myself"] { background-color: rgba(187, 218, 255, 0.25); /* Tomorrow Night Blue: Blue 25% */ } /* @end */ /* @group mIRC Color Codes */ /* @group Foreground Colors */ .effect[data-foreground-color="0"] { color: #D6D6D6 !important; /* Tomorrow: Selection */ } .effect[data-foreground-color="1"] { color: #4D4D4C !important; /* Tomorrow: Foreground */ } .effect[data-foreground-color="2"] { color: #4271AE !important; /* Tomorrow: Blue */ } .effect[data-foreground-color="3"] { color: #718C00 !important; /* Tomorrow: Green */ } .effect[data-foreground-color="4"] { color: #C82829 !important; /* Tomorrow: Red */ } .effect[data-foreground-color="5"] { color: #DE935F !important; /* Tomorrow Night: Red */ } .effect[data-foreground-color="6"] { color: #8959A8 !important; /* Tomorrow: Purple */ } .effect[data-foreground-color="7"] { color: #F5871F !important; /* Tomorrow: Orange */ } .effect[data-foreground-color="8"] { color: #EAB700 !important; /* Tomorrow: Yellow */ } .effect[data-foreground-color="9"] { color: #B9CA4A !important; /* Tomorrow Night Bright: Green */ } .effect[data-foreground-color="10"] { color: #3E999F !important; /* Tomorrow: Aqua */ } .effect[data-foreground-color="11"] { color: #66CCCC !important; /* Tomorrow Night Eighties: Aqua */ } .effect[data-foreground-color="12"] { color: #7AA6DA !important; /* Tomorrow Night Bright: Blue */ } .effect[data-foreground-color="13"] { color: #C397D8 !important; /* Tomorrow Night Bright: Purple */ } .effect[data-foreground-color="14"] { color: #8E908C !important; /* Tomorrow: Comment */ } .effect[data-foreground-color="15"] { color: #C5C8C6 !important; /* Tomorrow Night: Foreground */ } /* @end */ /* @group Background Colors */ .effect[data-background-color="0"] { background-color: #D6D6D6 !important; /* Tomorrow: Selection */ } .effect[data-background-color="1"] { background-color: #4D4D4C !important; /* Tomorrow: Foreground */ } .effect[data-background-color="2"] { background-color: #4271AE !important; /* Tomorrow: Blue */ } .effect[data-background-color="3"] { background-color: #718C00 !important; /* Tomorrow: Green */ } .effect[data-background-color="4"] { background-color: #C82829 !important; /* Tomorrow: Red */ } .effect[data-background-color="5"] { background-color: #DE935F !important; /* Tomorrow Night: Red */ } .effect[data-background-color="6"] { background-color: #8959A8 !important; /* Tomorrow: Purple */ } .effect[data-background-color="7"] { background-color: #F5871F !important; /* Tomorrow: Orange */ } .effect[data-background-color="8"] { background-color: #EAB700 !important; /* Tomorrow: Yellow */ } .effect[data-background-color="9"] { background-color: #B9CA4A !important; /* Tomorrow Night Bright: Green */ } .effect[data-background-color="10"] { background-color: #3E999F !important; /* Tomorrow: Aqua */ } .effect[data-background-color="11"] { background-color: #66CCCC !important; /* Tomorrow Night Eighties: Aqua */ } .effect[data-background-color="12"] { background-color: #7AA6DA !important; /* Tomorrow Night Bright: Blue */ } .effect[data-background-color="13"] { background-color: #C397D8 !important; /* Tomorrow Night Bright: Purple */ } .effect[data-background-color="14"] { background-color: #8E908C !important; /* Tomorrow: Comment */ } .effect[data-background-color="15"] { background-color: #C5C8C6 !important; /* Tomorrow Night: Foreground */ } /* @end */ /* @end */ /* vim: set foldmethod=marker: vim: set foldmarker=@group,@end: vim: set nofoldenable: */ ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/Varieties/Light/inlineMedia.css ================================================ /* @group Images */ .inlineImage, .inlineVideo, .inlineVideoService { margin-top: 8px !important; margin-bottom: 8px !important; } .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin-right: 15px; margin-left: 10px; min-width: 40px; max-width: 90%; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { cursor: pointer; border-radius: 5px; border: 2px solid #a1a1a1; color: #a1a1a1; display: inline-block; line-height: 14px; font-size: 15px; font-family: "Helvetica Neue" !important; text-indent: 7px; width: 16px; height: 16px; float: left; padding-right: 7px; padding-left: 0px; } /* @end */ ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/Varieties/Light/settings.plist ================================================ Appearance light Channel View Overlay Color #00000033 Indentation Offset 6 Nickname Color Style HSL-light Underlying Window Color #FFFFFF ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/copyright.txt ================================================ This style is based on the Simplified Light style shipped with Textual as of version 4.1.4. The colors are taken from the Tomorrow and base16 themes of Chris Kempson. For more information, see: https://github.com/FroZnShiva/Textual-Style-Tomorrow_Night_Eighties Simplified Light original copyright: This style is a modified and stripped down version of the "Simplified" theme developed by "Cowboy" Ben Alman (http://benalman.com/). Copyright © 2010, 2011, 2012. ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/scripts.js ================================================ /* Defined in: "Textual.app -> Contents -> Resources -> JavaScript -> API -> core.js" */ Textual.viewBodyDidLoad = function() { Textual.fadeOutLoadingScreen(1.00, 0.95); } Textual.messageAddedToView = function(line, fromBuffer) { var element = document.getElementById("line-" + line); ConversationTracking.updateNicknameWithNewMessage(element); } Textual.nicknameSingleClicked = function(e) { ConversationTracking.nicknameSingleClickEventCallback(e); } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Tomorrow/settings.plist ================================================ Template Engine Versions default 4 ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Total Sublime/Templates/encryptedMessageLock.mustache ================================================ [encrypted] ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Total Sublime/copyright.txt ================================================ Developed by Daniel Bird ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Total Sublime/design.css ================================================ /* Inline media */ @import "inlineMedia.css"; /* Basic Body Structure */ :root { supported-color-schemes: dark; } * { margin: 0; padding: 0; font-size: 100%; word-wrap: break-word; word-break: break-word; line-height: 1.7em; } body { color: #e7e7e7; height: 100%; z-index: 100; font-size: 11px; overflow: hidden; background-color: #272822; font-family: "Lucida Grande"; } #body { left: 0; right: 0; bottom: 0; opacity: 0; width: 100%; max-height: 100%; overflow-y: auto; z-index: 100; position: absolute; opacity: 0; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Only factor in height of topic bar when selected == true because that is only time the topic is displayed according to the logic defined by baseLayout.css */ body[data-selected="true"][data-view-type="channel"] #body { max-height: calc(100% - 2.4em); /* height minus topic */ } div.line { margin-top: -1px; clear: both; } body[dir="rtl"] .sender { display: inline-block; } /* Scrolling */ body[data-custom-scroller="true"]::-webkit-scrollbar { width: 17px; } body[data-custom-scroller="true"]::-webkit-scrollbar:horizontal { height: 0; } body[data-custom-scroller="true"]::-webkit-scrollbar-track { background: #393939; box-shadow: inset 1px 0px 0px 0px #4b4b4b; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb { background-color: #7c7c7c; border: 4px solid transparent; border-left: 5px solid transparent; border-radius: 20px; background-clip: content-box; } body[data-custom-scroller="true"]::-webkit-scrollbar-thumb:hover { background-color: #b0b0b0; } /* Loading Screen */ #loadingScreen { position: absolute; top: 45%; left: calc(50% - (320px / 2)); /* half of width + padding */ width: 300px; font-size: 18px; background: #181818; border: 1px solid #333333; border-radius: 5px; padding: 5px 10px; opacity: 1; /* Set by JavaScript */ -webkit-transition: opacity 0.8s linear; } /* Time */ body[dir="ltr"] .time { font-size: 9px; color: #999; white-space: nowrap; text-transform:lowercase; padding-right: 10px; } body[dir="rtl"] .time { color: #999; white-space: nowrap; padding-left: 0.4em; display: inline-block; } /* Encryption Lock */ .encryptionLock img { float: right; margin: 0; padding: 0; height: 11px; margin-top: 2px; padding-left: 10px; } /* Links */ a { color: #2199ff; text-decoration: none; } a:hover { color: #70baff; } /* Topic Bar */ #topicBar { top: 0; left: 0; right: 0; opacity: 0; /* Set by JavaScript */ z-index: 400; color: #555; position: fixed; padding: 2px 0.5em 3px; box-shadow: 0 1px 5px #444; border-bottom: 1px solid #222; text-shadow: 1px 1px rgba(10, 10, 10, 0.7); background: #171814; -webkit-transition: opacity 0.8s linear; -webkit-font-smoothing: subpixel-antialiased; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* Topic bar hover additions contributed with permission from the project: */ #topicBar:hover { overflow: visible; white-space: normal; } #topicBar a, #topicBar span.channel { color: #8E8E8E; border-color: #8E8E8E; text-shadow: 1px 1px rgba(10, 10, 10, 0.7); } /* Remember Line */ #mark { position: relative; clear: both; z-index: 295; margin: 10px 0; border-bottom: 1px dotted; border-color: #444; -webkit-transition: 0.2s linear; } /* Selected User Message */ .sender { cursor: pointer; } div.line[data-line-type="privmsg"]:not(.selectedUser), div.line[data-line-type="action"]:not(.selectedUser) { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; } div.line[data-line-type="privmsg"] .time:not(.selectedUser), div.line[data-line-type="action"] .time:not(.selectedUser) { transition: color 0.5s; } div.line.selectedUser[data-highlight="false"] .time { transition: color 0.5s ease-in; color: #aaa; } div.line.selectedUser[data-line-type="privmsg"][data-member-type="myself"] .time { color: #fff; } div.line.selectedUser[data-highlight="false"] { transition-property: border-top, border-bottom, background-color; transition-duration: 0.5s, 0.5s, 0.5s; z-index: 190; position: relative; border-top: 0px !important; border-bottom: 0px !important; background-color: #34352d !important; } /* Message buffer loading animation */ .message_buffer_loading_indicator { text-align: center; height: 42px; position: relative; z-index: 200; } .message_buffer_loading_indicator span { font-family: Optima !important; font-size: 35px; font-weight: 600; letter-spacing: 5px; line-height: 43px; color: #797979; -webkit-animation: ellipsis-period 1.5s infinite; animation: ellipsis-period 1.5s infinite; } .message_buffer_loading_indicator span:nth-child(1) { -webkit-animation-delay: 0.0s; animation-delay: 0.0s; } .message_buffer_loading_indicator span:nth-child(2) { -webkit-animation-delay: 0.1s; animation-delay: 0.1s; } .message_buffer_loading_indicator span:nth-child(3) { -webkit-animation-delay: 0.2s; animation-delay: 0.2s; } @keyframes ellipsis-period { 0% { opacity: 0.2; } 20% { opacity: 1.0; } 100% { opacity: 0.2; } } /* Message buffer session indicator */ .date_indicator , .session_indicator { display: flex; display: -webkit-flex; padding: 0.5em 0; } .date_indicator > hr, .session_indicator > hr { background: #444; border: 0; height: 1px; margin-top: 0.6em; flex: 1; -webkit-flex: 1; } .date_indicator > span, .session_indicator > span { font-style: oblique; margin: 0 1em; color: #919191; } .date_indicator + #mark, .session_indicator + #mark { display: none; } /* NOTICE/CTCP/WALLOPS */ div.line[data-line-type="ctcp"], div.line[data-line-type="notice"], div.line[data-line-type="wallops"] { color: #f00; z-index: 191; background: #400; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #811; border-bottom: 1px solid #811; } div.line[data-line-type="notice"] .sender { color: #f00; } /* PRIVMSG */ div.line[data-line-type="privmsg"][data-highlight="false"] { padding: 3px 5px 3px 5px; } div.line[data-line-type="privmsg"][data-highlight="true"], div.line[data-line-type="action"][data-highlight="true"] { padding: 2px 5px 2px 5px; border-top: 1px solid #988C00; border-bottom: 1px solid #988C00; background-color: #362C00 !important; } div.line[data-line-type="privmsg"] .message { color: #aaa; } div.line[data-line-type="privmsg"][data-member-type="myself"] .message { color: #fff; } div.line[data-line-type="privmsg"] .sender { } /* ACTION */ div.line[data-line-type="action"] { padding: 3px 5px 3px 5px; } div.line[data-line-type="action"] .message { color: #aaa; } div.line[data-line-type="action"] .sender { color: #aaa; } body[dir="ltr"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-right: 0.4em; } body[dir="ltr"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="action"] .sender:before { content: "\2022"; margin-left: 0.4em; } body[dir="rtl"] div.line[data-line-type="action"] .sender:after { content: ""; /* margin-left: 0.4em; */ } /* DEBUG/INVITE */ div.line[data-line-type="invite"], div.line[data-line-type="debug"], div.line[data-line-type="dcc-file-transfer"], div.line[data-line-type="off-the-record-encryption-status"] { color: #777; z-index: 190; background: #222; position: relative; padding: 2px 5px 2px 5px; border-top: 1px solid #444; border-bottom: 1px solid #444; } /* off-the-record-encryption-status Message Event */ div.line[data-line-type="off-the-record-encryption-status"] .message { color: #ff0000; font-weight: 700; } /* Message of the Day (MOTD) */ /* 720, 721, 722 are used by ShadowIRCd for Oper MOTD. */ /* 372, 375, 376 are normal MOTD shared by several IRCds. */ div.line[data-command="372"], div.line[data-command="721"] { border: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="375"], div.line[data-command="720"] { /* Start. */ border-bottom: none; padding-top: 2px; padding-bottom: 3px; } div.line[data-command="376"], div.line[data-command="722"] { /* End. */ border-top: none; padding-top: 3px; padding-bottom: 3px; } div.line[data-command="372"] .message, div.line[data-command="375"] .message, div.line[data-command="376"] .message div.line[data-command="720"] .message, div.line[data-command="721"] .message, div.line[data-command="722"] .message { font-family: "Menlo" !important; } /* GENERAL EVENT */ div.line[data-line-type="join"], div.line[data-line-type="part"], div.line[data-line-type="kick"], div.line[data-line-type="quit"], div.line[data-line-type="kill"], div.line[data-line-type="nick"], div.line[data-line-type="mode"], div.line[data-line-type="topic"], div.line[data-line-type="website"] { padding: 3px 5px 3px 5px; color: #666; } body[dir="ltr"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="kick"] .message:before, body[dir="ltr"] div.line[data-line-type="part"] .message:before, body[dir="ltr"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-right: 0.4em; */ } body[dir="ltr"] div.line[data-line-type="nick"] .message:before { content:"•"; color: #0c0; /* margin-right: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="join"] .message:before { content: "→"; color: #0c0; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="kick"] .message:before, body[dir="rtl"] div.line[data-line-type="part"] .message:before, body[dir="rtl"] div.line[data-line-type="quit"] .message:before { content: "←"; color: #e00; /* margin-left: 0.4em; */ } body[dir="rtl"] div.line[data-line-type="nick"] .message:before { content:"•"; color: #0c0; /* margin-left: 0.4em; */ } /* Nickname Colors */ div.line[data-line-type="privmsg"] .sender[data-member-type="myself"] { color: #ff6f6f; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Total Sublime/inlineMedia.css ================================================ /* Images */ .inlineImage .content, .inlineVideo .content, .inlineVideoService .content, .inlineHTML .content { display: inline-block; float: left; margin-right: 12px; margin-left: 10px; min-width: 40px; max-width: 90%; } .inlineImage .closeButton, .inlineVideo .closeButton, .inlineVideoService .closeButton, .inlineHTML .closeButton { cursor: pointer; border-radius: 5px; border: 2px solid #a1a1a1; color: #a1a1a1; display: inline-block; line-height: 14px; font-size: 15px; font-family: "Helvetica Neue" !important; text-indent: 7px; width: 16px; height: 16px; float: left; padding-right: 7px; padding-left: 0px; } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Total Sublime/scripts.js ================================================ /* Defined in: "Textual.app -> Contents -> Resources -> JavaScript -> API -> core.js" */ Textual.viewBodyDidLoad = function() { Textual.fadeOutLoadingScreen(1.00, 0.95); } Textual.messageAddedToView = function(line, fromBuffer) { var element = document.getElementById("line-" + line); ConversationTracking.updateNicknameWithNewMessage(element); } Textual.nicknameSingleClicked = function(e) { ConversationTracking.nicknameSingleClickEventCallback(e); } ================================================ FILE: Sources/App/Resources/Styling/Bundled Styles/Total Sublime/settings.plist ================================================ Appearance dark Channel View Overlay Color #00000066 Force Invert Sidebars Indentation Offset 0 Nickname Color Style HSL-dark Nickname Format %@%n: Template Engine Versions default 4 Underlying Window Color #000000 ================================================ FILE: Sources/App/Resources/Styling/JavaScript/API/core.js ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "use strict"; var Textual = {}; /* *********************************************************************** */ /* View Callbacks */ /* *********************************************************************** */ /* Callbacks for each WebView in Textual. */ /* These callbacks are limited to the context of this view. The view can represent either a server console, a channel, or a private message. See viewInitiated() for information about determining which type of view this is. */ /* In the context of these callbacks, "client" or "associated client" is an abstract concept. When you create a new connection in Textual, you set it up by entering where to connect to and what your identity will be (nickname, username, etc.). You also choose channels to join. You may configure it other ways as well. Client is an encapsulation of this. Client is the stateful part of the connection that keeps track of everything. */ /* viewInitiated(): @viewType: Type of view: Server console, channel, or private message. Possible values: "server", "channel", "query" — query = private message. @clientHash: A unique identifier for the client. @viewHash: A unique identifier for the view. null for server console (use clientHash for that). @viewName: Name of view: Channel name, nickname for a private message, or null for server console. */ Textual.viewInitiated = function(viewType, clientHash, viewHash, viewName) {}; Textual.messageAddedToView = function(lineNumber, fromBuffer) {}; Textual.messageRemovedFromView = function(lineNumber) {}; Textual.historyIndicatorAddedToView = function() {}; Textual.historyIndicatorRemovedFromView = function() {}; Textual.topicBarValueChanged = function(newTopic) {}; Textual.viewFinishedLoading = function() {}; Textual.viewFinishedLoadingHistory = function() {}; Textual.viewFinishedReload = function() {}; Textual.viewFontSizeChanged = function(bigger) {}; Textual.viewPositionMovedToBottom = function() {}; Textual.viewPositionMovedToHistoryIndicator = function() {}; Textual.viewPositionMovedToLine = function(lineNumber) {}; Textual.viewPositionMovedToTop = function() {}; /* This function is called when one of two conditions are met: 1. The day has changed by reaching midnight (00:00) 2. The system clock changes For the second condition, Textual does not make an effort to compare if the day has in-fact changed. It merely passes the change down to the style to let it know. */ Textual.dateChanged = function(dayYear, dayMonth, dayDay) {}; /* This function is not called by Textual itself, but by WebKit. It is appended to as the function to call during onload phase. It is used by the newer templates to replace viewDidFinishLoading() as the function responsible for fading out the loading screen. */ Textual.viewBodyDidLoad = function() {}; /* *********************************************************************** */ /* Appearance */ /* *********************************************************************** */ /* app.appearance() can be used to determine the current appearance of Textual. The return value is a string. For example: "light" or "dark" */ /* Textual.appearanceDidChange() can be used to observe changes to the appearance. */ // app.appearance(callbackFunction); /* A boolean entry named "Post Textual.appearanceDidChange() Notifications" must be added to the settings.plist file in order to enable use of this callback. */ /* A style that has a variety for each appearance type will never receive a callback. Those styles will have the best variety automatically selected by Textual and the style will be reloaded to refresh the view. */ Textual.appearanceDidChange = function(changedTo) {}; /* *********************************************************************** */ /* Event Handling */ /* *********************************************************************** */ /* handleEvent() allows a style to receive status information about several actions going on behind the scenes. The following event tokens are currently supported: serverConnecting - Connecting to IRC serverConnected - Connected to IRC serverDisconnecting - Disconnecting from IRC serverDisconnected - Disconnected from IRC channelJoined - Channel joined channelParted — Channel parted channelMemberAdded — Member added to the channel (joined) channelMemberRemoved — Member removed from the channel (parted) nicknameChanged — Nickname of local user (you) changed THESE EVENTS ARE PUSHED WHEN THEY OCCUR. When a style is reloaded, these events are not sent again. Use sessionStorage or some other means to save them if they are important. */ /* A boolean entry named "Post Textual.handleEvent() Notifications" must be added to the settings.plist file in order to enable use of this callback. */ Textual.handleEvent = function(eventToken) {}; /* *********************************************************************** */ /* Application Object */ /* *********************************************************************** */ /* The "app" object provides a communication channel between the style and Textual. Functions are performed asynchronously which means a callback function is required to receive return values. If a callback function is required, then the callback function should take one argument, which is a variable defining the return value. The type of the return value will vary depending on what the function does. Example: app.inlineMediaEnabledForView( function(returnValue) { console.log(returnValue); } ) */ /* app.inlineMediaEnabledForView(callbackFunction) Is inline media enabled? Return: boolean */ /* app.serverIsConnected(callbackFunction) Is the client connected? Return: boolean */ /* app.channelIsActive(callbackFunction) Is the channel or private message active? For channel: Is the channel joined? For private message: Is the user online? Return: boolean */ /* app.serverChannelCount(callbackFunction) Number of channels associated with the client. Notice: This number includes private messages. Return: integer */ /* app.channelName(callbackFunction) Name of the channel, nickname for a private message, or null for server console. Return: nullable string */ /* app.networkName(callbackFunction) Name of the network connected to. User configurable connection name is returned if the client isn't connected. Example: "freenode IRC Network" or "My Connection" Return: string */ /* app.serverAddress(callbackFunction) Address of the server connected to. User configurable server address is returned if the client isn't connected. Example: "chat.freenode.net" Return: string */ /* app.localUserNickname(callbackFunction) Nickname of the local user (you). Return: string */ /* app.localUserHostmask(callbackFunction) Hostmask of the local user (you). Notice: Hostmask is not available until at least one channel is joined. Return: nullable string */ /* *********************************************************************** */ /* Print Debug Messages */ /* *********************************************************************** */ // app.logToConsole() - Log a message to the macOS system-wide console. /* The app.printDebugInformation* functions documented below also call messageAddedToView() which means calling them from within it will create an infinite loop. */ // app.printDebugInformationToConsole(message) — Show a debug message to the user in the server console. // app.printDebugInformation(message) — Show a debug message to the user in this view. /* *********************************************************************** */ /* Style Settings */ /* *********************************************************************** */ /* Textual provides styles the ability to store values within a key-value store which is shared amongst all views. This store is saved in Textual's preference file and will persist even if the style is renamed, removed, or replaced. - Enabling: A string entry named "Key-value Store Name" must be added to the settings.plist file in order to enable use of this feature. The value of this entry is the name to save the store under. Preferably, this will be the name of the style, but a different name can be entered so that multiple varieties of a style can share the same store. - Notice: • null (not undefined) is returned when a value does not exist for a key. • To remove the value of a key from the store, set null as its value. */ // app.styleSettingsRetrieveValue(key, callbackFunction) — Retrieve value of /key/ from the key-value store. // app.styleSettingsSetValue(key, value, callbackFunction) — Set /value/ to /key/ in the key-value store. /* This function is performed when a style setting changed. It is performed on all views, including the one that was responsible for changing the value. */ Textual.styleSettingDidChange = function(changedKey) {}; /* *********************************************************************** */ /* Textual Preferences */ /* *********************************************************************** */ /* A boolean entry named "Post Textual.preferencesDidChange() Notifications" must be added to the settings.plist file in order to enable use of this callback. */ /* This callback is rate-limit at one call per-second, per-view. */ /* This callback exists for extremely specific use cases. In general, if you need something that doesn't exist above, then it's better to ask for it to be added and not rely on this callback. */ Textual.preferencesDidChange = function() {}; ================================================ FILE: Sources/App/Resources/Styling/JavaScript/API/corePrivate.js ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "use strict"; /* ************************************************** */ /* */ /* DO NOT OVERRIDE ANYTHING BELOW THIS LINE */ /* */ /* ************************************************** */ /* Private objects */ var _Textual = {}; /* Resource management */ Textual.initializeCore = function(resourcesPath) { Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/core/clickMenuSelection.js"); Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/core/documentBody.js"); Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/core/events.js"); Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/core/inlineMedia.js"); Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/core/messageBuffer.js"); Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/core/scrollTo.js"); Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/scroller/state.js"); /* Only load auto scroller if we believe this is WebKit2 */ if (window.webkit && typeof window.webkit.messageHandlers === "undefined") { Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/scroller/automaticEmpty.js"); } else { Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/scroller/automatic.js"); } Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/conversationTracking.js"); Textual.includeScriptResourceFile(resourcesPath + "/JavaScript/API/private/scriptSink.js"); }; Textual.includeStyleResourceFile = function(file) { if (/loaded|complete/.test(document.readyState)) { var newFile = document.createElement("link"); newFile.charset = "UTF-8"; newFile.href = file; newFile.media = "screen"; newFile.rel = "stylesheet"; newFile.type = "text/css"; document.getElementsByTagName("HEAD")[0].appendChild(newFile); } else { document.write(''); } }; Textual.includeScriptResourceFile = function(file) { if (/loaded|complete/.test(document.readyState)) { var newFile = document.createElement("script"); newFile.setAttribute("charset", "UTF-8"); newFile.charset = "UTF-8"; newFile.src = file; newFile.type = "text/javascript"; document.getElementsByTagName("HEAD")[0].appendChild(newFile); } else { document.write(' {{#activeStyleJSFiles}} {{/activeStyleJSFiles}} {{! The elements "body," "messageBuffer," "loadingScreen," "topicBar," and "mark" must always remain the same id= as these elements are used internally by Textual. }}
View is Loading…
{{#isChannelView}}
(No Topic)
{{/isChannelView}}
================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/dateIndicator.mustache ================================================

{{dateIndicatorMessage}}
================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/encryptedMessageLock.mustache ================================================ {{! Not handled by default. }} ================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/formattedMessageFragment.mustache ================================================ {{! When this is a new fragment, but not the last fragment, then we use the following *ClosedAtStart tags to close any formatting that may be open for the previous fragment. }} {{#fragmentIsBoldClosedAtStart}}
{{/fragmentIsBoldClosedAtStart}} {{#fragmentIsItalicizedClosedAtStart}}{{/fragmentIsItalicizedClosedAtStart}} {{#fragmentIsMonospaceClosedAtStart}}{{/fragmentIsMonospaceClosedAtStart}} {{#fragmentIsStruckthroughClosedAtStart}}{{/fragmentIsStruckthroughClosedAtStart}} {{#fragmentIsUnderlinedClosedAtStart}}{{/fragmentIsUnderlinedClosedAtStart}} {{#fragmentTextColorClosedAtStart}} {{/fragmentTextColorClosedAtStart}} {{#fragmentTextColorOpened}} {{/fragmentTextColorOpened}} {{#fragmentIsBoldOpened}}{{/fragmentIsBoldOpened}} {{#fragmentIsItalicizedOpened}}{{/fragmentIsItalicizedOpened}} {{#fragmentIsMonospaceOpened}}{{/fragmentIsMonospaceOpened}} {{#fragmentIsStruckthroughOpened}}{{/fragmentIsStruckthroughOpened}} {{#fragmentIsUnderlinedOpened}}{{/fragmentIsUnderlinedOpened}} {{#inlineNicknameMatchFound}} {{inlineNicknameUserModeSymbol}} {{/inlineNicknameMatchFound}} {{{messageFragment}}} {{#inlineNicknameMatchFound}} {{/inlineNicknameMatchFound}} {{! When this is the end of the last fragment, we terminate all }} {{#fragmentIsBoldClosedAtEnd}}{{/fragmentIsBoldClosedAtEnd}} {{#fragmentIsItalicizedClosedAtEnd}}{{/fragmentIsItalicizedClosedAtEnd}} {{#fragmentIsMonospaceClosedAtEnd}}{{/fragmentIsMonospaceClosedAtEnd}} {{#fragmentIsStruckthroughClosedAtEnd}}{{/fragmentIsStruckthroughClosedAtEnd}} {{#fragmentIsUnderlinedClosedAtEnd}}{{/fragmentIsUnderlinedClosedAtEnd}} {{#fragmentTextColorClosedAtEnd}} {{/fragmentTextColorClosedAtEnd}} ================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/historyIndicator.mustache ================================================
================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/messageBufferLoadingIndicator.mustache ================================================
================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/messageBufferSessionIndicator.mustache ================================================ {{! This template is deprecated. Use sessionIndicator.mustache instead. }} {{> sessionIndicator}} ================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/newMessagePostedWithSender.mustache ================================================ {{#showDateIndicator}} {{> dateIndicator}} {{/showDateIndicator}}

{{formattedTimestamp}} {{#isEncrypted}} {{> encryptedMessageLock}} {{/isEncrypted}} {{formattedNickname}} {{{formattedMessage}}} {{#inlineMediaEnabled}} {{/inlineMediaEnabled}}

{{#showSessionIndicator}} {{> sessionIndicator}} {{/showSessionIndicator}} ================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/newMessagePostedWithoutSender.mustache ================================================ {{#showDateIndicator}} {{> dateIndicator}} {{/showDateIndicator}}

{{formattedTimestamp}} {{{formattedMessage}}}

{{#showSessionIndicator}} {{> sessionIndicator}} {{/showSessionIndicator}} ================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/renderedChannelNameLinkResource.mustache ================================================ {{channelName}} ================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/renderedStandardAnchorLinkResource.mustache ================================================ {{{anchorTitle}}} ================================================ FILE: Sources/App/Resources/Styling/Style Default Templates/Version 4/sessionIndicator.mustache ================================================ {{! "message_buffer_session_indicator" is kept for backwards compatibility (<=7.0.10) }}

{{sessionIndicatorMessage}}
================================================ FILE: Sources/App/Resources/User Interface/Appearance/TDCChannelSpotlightAppearance.plist ================================================ Common Search Field controlTextColor type 3 value controlTextColor Search Result rowEmphasized keyboardShortcutDeselectedOffset 0 keyboardShortcutSelectedOffset 3 CommonModern inheritFrom Common Search Field noResultsTextColor type 3 value secondaryLabelColor CommonModernDark inheritFrom CommonModern Search Result rowEmphasized channelNameTextColor type 1 value 0.9 channelDescriptionTextColor type 1 value 0.7 keyboardShortcutTextColor type 1 value 0.9 selectedTextColor type 1 value 1.0 CommonModernLight inheritFrom CommonModern Search Result channelNameTextColor type 1 value 0.25 channelDescriptionTextColor type 1 value 0.35 keyboardShortcutTextColor type 1 value 0.25 selectedTextColor type 1 value 1.0 BigSurDark inheritFrom CommonModernDark BigSurLight inheritFrom CommonModernLight ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TDCChannelSpotlightAppearanceTemplate.plist ================================================ TEMPLATE APPEARANCE (NOT USED BY ANYTHING) Search Field controlTextColor type 2 value 0.0 0.0 0.0 completionTextColor type 2 value 0.0 0.0 0.0 noResultsTextColor type 2 value 0.0 0.0 0.0 Search Result selectionColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 rowEmphasized channelNameTextColor type 2 value 0.0 0.0 0.0 channelDescriptionTextColor type 2 value 0.0 0.0 0.0 keyboardShortcutTextColor type 2 value 0.0 0.0 0.0 keyboardShortcutDeselectedOffset 0 keyboardShortcutSelectedOffset 0 selectedTextColor type 2 value 0.0 0.0 0.0 ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TVCMainWindowAppearance.plist ================================================ Common titlebarAccessoryViewLeftMargin 3 titlebarAccessoryViewRightMargin 5 channelViewOverlayDefaultBackgroundColor activeWindow type 1 value 0.0 0.3 inactiveWindow type 1 value 0.0 0.3 defaultWindowSize width 800 height 474 splitViewDividerColor type 1 value 0.65 CommonModern inheritFrom Common titlebarAccessoryViewBackgroundColor activeWindow type 2 value 0.462 0.462 0.462 CommonModernDark inheritFrom CommonModern loadingScreenBackgroundColor type 3 value windowBackgroundColor CommonModernLight inheritFrom CommonModern loadingScreenBackgroundColor type 1 value 1.0 BigSurDark inheritFrom CommonModernDark BigSurLight inheritFrom CommonModernLight ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TVCMainWindowAppearanceTemplate.plist ================================================ TEMPLATE APPEARANCE (NOT USED BY ANYTHING) channelViewOverlayDefaultBackgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 defaultWindowSize width 0 height 0 loadingScreenBackgroundColor type 2 value 0.0 0.0 0.0 splitViewDividerColor type 2 value 0.0 0.0 0.0 titlebarAccessoryViewBackgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 titlebarAccessoryViewLeftMargin 0 titlebarAccessoryViewRightMargin 0 ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TVCMainWindowTextViewAppearance.plist ================================================ Common Text View inset width 1 height 2 Background View contentBorderPadding 23 CommonModern inheritFrom Common Text View font name System size 12 fontLarge name System size 14 fontExtraLarge name System size 16 fontHumongous name System size 24 CommonModernDark inheritFrom CommonModern Background View backgroundColor type 2 value 0.248 0.248 0.248 dividerColor type 2 value 0.150 0.150 0.150 Text View normalTextColor type 1 value 1.0 placeholderTextColor type 2 value 0.660 0.660 0.660 backgroundColor activeWindow type 2 value 0.386 0.386 0.386 inactiveWindow type 2 value 0.386 0.386 0.386 outsidePrimaryShadowColor activeWindow type 1 value 0.0 0.10 inactiveWindow type 1 value 0.0 0.10 outsidePrimaryShadowColor@2x activeWindow type 1 value 0.0 0.15 inactiveWindow type 1 value 0.0 0.15 CommonModernLight inheritFrom CommonModern Background View backgroundColor type 2 value 0.957 0.957 0.957 dividerColor type 1 value 0.65 Text View normalTextColor type 1 value 0.15 placeholderTextColor type 1 value 0.50 backgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 insideShadowColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 insideGradient activeWindow type 2 value 0.992 0.992 0.992 type 2 value 0.988 0.988 0.988 inactiveWindow type 2 value 0.992 0.992 0.992 type 2 value 0.988 0.988 0.988 outsidePrimaryShadowColor activeWindow type 1 value 0.0 0.15 inactiveWindow type 1 value 0.0 0.15 outsideSecondaryShadowColor activeWindow type 1 value 0.0 0.10 inactiveWindow type 1 value 0.0 0.10 outsideSecondaryShadowColor@2x activeWindow type 1 value 0.0 0.06 inactiveWindow type 1 value 0.0 0.06 BigSurDark inheritFrom CommonModernDark BigSurLight inheritFrom CommonModernLight ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TVCMainWindowTextViewAppearanceTemplate.plist ================================================ TEMPLATE APPEARANCE (NOT USED BY ANYTHING) Background View backgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 dividerColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 contentBorderPadding 0 Text View inset width 0 height 0 normalTextColor type 2 value 0.0 0.0 0.0 placeholderTextColor type 2 value 0.0 0.0 0.0 backgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 outlineColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 insideShadowColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 insideGradient activeWindow type 2 value 0.0 0.0 0.0 type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 type 2 value 0.0 0.0 0.0 outsidePrimaryShadowColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 outsideSecondaryShadowColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 font name System size 12 fontLarge name System size 12 fontExtraLarge name System size 12 fontHumongous name System size 12 ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TVCMemberListAppearance.plist ================================================ Common defaultWidth 120 minimumWidth 120 maximumWidth 300 Mark Badge leftMargin 0 Member Cell rowEmphasized CommonModern inheritFrom Common Member Cell font name System size 12 fontSelected name System size 12 Mark Badge font name Helvetica size 12 weight 15 fontSelected name Helvetica size 12 weight 15 CommonModernDark inheritFrom CommonModern Member Cell rowEmphasized normalTextColor activeWindow type 1 value 0.9 inactiveWindow type 1 value 0.9 awayTextColor activeWindow type 1 value 0.4 inactiveWindow type 1 value 0.4 selectedTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 Mark Badge normalBackgroundColor activeWindow type 1 value 0.15 inactiveWindow type 1 value 0.15 selectedBackgroundColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 normalTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 selectedTextColor activeWindow type 1 value 0.1 0.1 0.1 inactiveWindow type 1 value 0.1 0.1 0.1 CommonModernLight inheritFrom CommonModern Member Cell normalTextColor activeWindow type 1 value 0.25 inactiveWindow type 1 value 0.25 awayTextColor activeWindow type 1 value 0.55 inactiveWindow type 1 value 0.55 selectedTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 Mark Badge normalBackgroundColor activeWindow type 2 value 0.232 0.232 0.232 0.7 inactiveWindow type 2 value 0.232 0.232 0.232 0.7 selectedBackgroundColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 normalTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 selectedTextColor activeWindow type 2 value 0.232 0.232 0.232 inactiveWindow type 2 value 0.232 0.232 0.232 BigSurDark inheritFrom CommonModernDark BigSurLight inheritFrom CommonModernLight ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TVCMemberListAppearanceTemplate.plist ================================================ TEMPLATE APPEARANCE (NOT USED BY ANYTHING) defaultWidth 0 minimumWidth 0 maximumWidth 0 selectionColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 User Cell rowEmphasized normalTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 awayTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 selectedTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 font activeWindow name System size 10 inactiveWindow name System size 10 fontSelected activeWindow name System size 10 inactiveWindow name System size 10 Mark Badge normalBackgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 selectedBackgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 normalTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 selectedTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 font activeWindow name System size 10 inactiveWindow name System size 10 fontSelected activeWindow name System size 10 inactiveWindow name System size 10 leftMargin 0 ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TVCServerListAppearance.plist ================================================ Common defaultWidth 190 minimumWidth 120 maximumWidth 300 Server Cell labelLeftMargin 5 rowEmphasized Channel Cell rowEmphasized CommonModern inheritFrom Common Server Cell font name SystemBold size 14 fontSelected name System size 14 Channel Cell rowHeight 20 font name System size 12 fontSelected name System size 12 Unread Badge font name SystemMonospace size 11 fontSelected name SystemMonospace size 11 minimumWidth 22 height 14 padding 6 CommonModernDark inheritFrom CommonModern Server Cell rowEmphasized normalTextColor activeWindow type 1 value 0.9 inactiveWindow type 1 value 0.9 disabledTextColor activeWindow type 1 value 0.4 inactiveWindow type 1 value 0.4 selectedTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 Channel Cell rowEmphasized normalTextColor activeWindow type 1 value 0.9 inactiveWindow type 1 value 0.9 disabledTextColor activeWindow type 1 value 0.4 inactiveWindow type 1 value 0.4 selectedTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 erroneousTextColor activeWindow type 2 value 0.85 0.0 0.0 inactiveWindow type 2 value 0.85 0.0 0.0 highlightTextColor activeWindow type 2 value 0.0 0.414 0.117 inactiveWindow type 2 value 0.0 0.414 0.117 Unread Badge normalBackgroundColor activeWindow type 1 value 0.15 inactiveWindow type 1 value 0.15 selectedBackgroundColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 highlightBackgroundColor activeWindow type 2 value 0.0117 0.1562 0.0 inactiveWindow type 2 value 0.0117 0.1562 0.0 normalTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 selectedTextColor activeWindow type 1 value 0.1 0.1 0.1 inactiveWindow type 1 value 0.1 0.1 0.1 highlightTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 CommonModernLight inheritFrom CommonModern Server Cell normalTextColor activeWindow type 1 value 0.25 inactiveWindow type 1 value 0.25 disabledTextColor activeWindow type 1 value 0.55 inactiveWindow type 1 value 0.55 selectedTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 Channel Cell normalTextColor activeWindow type 1 value 0.25 inactiveWindow type 1 value 0.25 disabledTextColor activeWindow type 1 value 0.55 inactiveWindow type 1 value 0.55 selectedTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 erroneousTextColor activeWindow type 2 value 0.8203 0.0585 0.0585 inactiveWindow type 2 value 0.8203 0.0585 0.0585 highlightTextColor activeWindow type 2 value 0.0 0.414 0.117 inactiveWindow type 2 value 0.0 0.414 0.117 Unread Badge normalBackgroundColor activeWindow type 2 value 0.232 0.232 0.232 0.7 inactiveWindow type 2 value 0.232 0.232 0.232 0.7 selectedBackgroundColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 highlightBackgroundColor activeWindow type 2 value 0.0 0.414 0.117 0.7 inactiveWindow type 2 value 0.0 0.414 0.117 0.7 normalTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 selectedTextColor activeWindow type 2 value 0.232 0.232 0.232 inactiveWindow type 2 value 0.232 0.232 0.232 highlightTextColor activeWindow type 1 value 1.0 inactiveWindow type 1 value 1.0 BigSurDark inheritFrom CommonModernDark BigSurLight inheritFrom CommonModernLight ================================================ FILE: Sources/App/Resources/User Interface/Appearance/TVCServerListAppearanceTemplate.plist ================================================ TEMPLATE APPEARANCE (NOT USED BY ANYTHING) defaultWidth 0 minimumWidth 0 maximumWidth 0 selectionColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 Server Cell labelLeftMargin 0 rowEmphasized selectionImage activeWindow type 1 value inactiveWindow type 1 value normalTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 disabledTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 selectedTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 font name System size 10 fontSelected name System size 10 Channel Cell rowEmphasized selectionImage activeWindow type 1 value inactiveWindow type 1 value normalTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 disabledTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 selectedTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 erroneousTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 highlightTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 font name System size 10 fontSelected name System size 10 Unread Badge normalBackgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 selectedBackgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 highlightBackgroundColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 normalTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 selectedTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 highlightTextColor activeWindow type 2 value 0.0 0.0 0.0 inactiveWindow type 2 value 0.0 0.0 0.0 font name System size 10 fontSelected name System size 10 minimumWidth 0 height 0 padding 0 ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCAboutDialog.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCAddressBookSheet.xib ================================================ Textual monitors Join, Part, and Quit activity on channels in which you are present to establish the activity pattern of someone that you would like to track. On servers that support it, Textual will also use commands such as ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCChannelBanListSheet.xib ================================================ entryMask entryAuthor entryCreationDate entryCreationDateString %{value1}@ entries ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCChannelInviteSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCChannelModifyModesSheet.xib ================================================ ti_ER ar_LY kok_IN mk_MK eo fr_CH sw so_ET gv ar_BH hy_AM_REVISED it_IT bg_BG ro es_HN en_BE is kw_GB kl ga_IE nl_NL uk fr_CA sk_SK es_AR en_MT fr_BE ca cs_CZ fr_FR en_ZA sl fa so pt et_EE eu_ES fi de_CH or gu_IN mt_MT nb ms es_CR as_IN ar_SA am pl hu lv ar_DZ ur pt_PT eu so_DJ de en_IN hr_HR ar_AE gu lt te_IN ar_KW sr_Latn de_AT en_US es_PR pl_PL uz_Cyrl_UZ ar_EG it_CH nl haw_US is_IS en_IE kl_GL ps zh_Hans_CN ar ru_UA th_TH fr_LU uz_Latn_UZ az bn_IN es_MX kk_KZ en_US_POSIX el_GR he es_UY nn_NO sr sk ur_PK es_PA sv_FI zh es_DO en_CA en_BW de_DE haw pa_IN sq kw ga ml mt af_ZA az_Latn ko_KR fa_AF ps_AF be_BY bn it sr_Latn_CS zh_Hans as ms_MY be sl_SI fa_IR es_EC kok es_PY ta_IN am_ET es_CO uz_Latn el zh_Hant_MO kk kn zh_Hant en es_PE pt_BR ar_LB en_NZ es_ES en_HK es_BO hu_HU sr_Cyrl nb_NO sv vi ti_ET bg om de_LU en_SG id_ID ur_IN uz da ru_RU es_US es_VE ko ja af ar_YE lt_LT om_KE zh_Hans_SG es_NI he_IL en_PK en_PH az_Cyrl_AZ en_AU az_Cyrl ta ar_MA sw_KE tr_TR zh_Hant_HK en_ZW de_BE mk pa da_DK es_GT es ar_IQ az_Latn_AZ so_SO lv_LV mr te sq_AL ml_IN uk_UA hi_IN ca_ES ar_TN id om_ET cs fo_FO hy_AM en_GB sr_Cyrl_CS gl_ES sw_TZ ro_RO cy fr ms_BN so_KE tr gl cy_GB ar_OM fo es_CL sv_SE ar_JO uz_Cyrl zh_Hant_TW et hi fi_FI nn th ar_SY ja_JP gv_GB hy en_VI kn_IN ti ar_QA es_SV hr ru ar_SD mr_IN vi_VN nl_BE or_IN NSAllRomanInputSourcesLocaleIdentifier ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCChannelModifyTopicSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCChannelPropertiesSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCChannelSpotlightController.xib ================================================ channelName keyboardShortcut unreadCountDescription Xcode.InterfaceBuilderKit.ClassDescriptions.Cocoa:IBLibrary-ArrayController ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCFileTransferDialog.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCHighlightEntrySheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCLicenseManagerDialog.xib ================================================ You can request a license key if you already own Textual 5 or a newer version in the Mac App Store. To download a new copy of Textual from the Mac App Store, click on your name in the store to access your purchases. ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCLicenseManagerMigrateAppStoreSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCLicenseManagerRecoverLostLicenseSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCLicenseUpgradeActivateSheet.xib ================================================ You recently checked the upgrade eligibility for the license key “%@“ and it was determined that you are eligible for a free upgrade. You recently checked the upgrade eligibility for the license key “%@“ and it was determined that you are eligible for a discounted upgrade. Click “Purchase Upgrade“ button to purchase the upgrade. Click “Activate Textual“ button to activate this license. ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCLicenseUpgradeDialog.xib ================================================ Textual 7, the next generation of the app, is now available as a paid upgrade. To make this upgrade as smooth as possible, you may be eligible for a free or discounted upgrade; depending on when you purchased the app. Click “Check Eligibility" to find out which you are eligible for. ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCLicenseUpgradeEligibilitySheet.xib ================================================ Congratulations, you are eligible for a discount! Click the “Purchase Upgrade” button to view the price of the upgrade and to purchase it. When your order is finished being processed, click the “Activate Textual“ button to continue. Congratulations, you are eligible for a free upgrade! Click the “Activate Textual“ button to continue. ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCNicknameColorSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCPreferences.xib ================================================ JohnDoe%{value1}@… NSUnarchiveFromData NSUnarchiveFromData NSUnarchiveFromData NSUnarchiveFromData NSUnarchiveFromData NSUnarchiveFromData %{value1}@ %{value2}@pt %{value1}@ %{value2}@pt NSNegateBoolean 1000 2000 3000 4000 5000 10000 20000 30000 40000 50000 The maximum number of messages, per-channel, that are accessible through the scrollback. Messages are not saved between app launches unless “Restore scrollback from previous session” in the Behavior section of Preferences is enabled. [%H:%M] [%H:%M:%S] [%I:%M:%S %p] [%m/%d/%Y -:- %I:%M:%S %p] NSNegateBoolean %n: %@%n: (%n) <%n> <%@%n> <%@%-9n> NSNegateBoolean The commands listed above are performed like any other: write a forward slash (/) followed by the name of the command plus any required arguments. Textual will perform the command and output the results accordingly. Compatibility Current Value: %{value1}@ Textual must periodically poll the channels that you are in to find out when the away status of each user changes. This preference defines the maximum number of users a channel can contain before the channel is skipped over. A lower value is always better especially when in a large number of channels. Current Value: %{value1}@ seconds Current Value: %{value1}@ seconds Joining too many channels at once may be detected as an attack by some IRC networks. You could be disconnected or banned as a result. It is recommended to use a moderate delay (~2.5 seconds) for both timers when joining a large number of channels. NSIsNotNil This preference works by disabling certain services. It doesn't know whether a specific image or video is safe. You should therefore not rely solely on this preference. NSIsNotNil Off-the-Record Messaging (OTR) increases the privacy of private messages by encrypting their contents so that only you and your chat partner can see what is written. It also provides a means to verify, with certainty, that the person you are communicating with are who they claim to be. string string string 100 500 1000 1500 2000 2500 3000 3500 4000 4500 5000 ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCPreferencesUserStyleSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCProgressIndicatorSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCServerChangeNicknameSheet.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCServerChannelListDialog.xib ================================================ predicate (channelName contains[c] $value OR channelTopicUnformatted contains[c] $value) channelName channelMemberCount channelTopicFormatted channelTopicUnformatted ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCServerEndpointListSheet.xib ================================================ The order of this table, from top to bottom, determines the order in which Textual connects to each server.

Drag and drop servers to change the order. The top-most server is always the primary. Attempt to secure the connection by performing a Transport Layer Security (TLS) negotiation. TLS is the modern equivalent of SSL (Secure Sockets Layer) NSAllRomanInputSourcesLocaleIdentifier ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCServerHighlightListSheet.xib ================================================ channelName renderedMessage timeLoggedFormatted ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCServerPropertiesSheet.xib ================================================ NSAllRomanInputSourcesLocaleIdentifier NSAllRomanInputSourcesLocaleIdentifier Textual will attempt to connect to the Tor anonymity network using the SOCKS5 proxy on port 9150 that is available when the “Tor Browser” application is open. When enabled, the SOCKS proxy that can be configured in the Network section of System Settings will be used with the “Automatic” proxy type selected. Current Value: %{value1}@ Current Value: %{value1}@ Textual is capable of automatically connecting to one or more alternate servers if the primary server fails to connect. Click the button presented below to modify the alternate servers for this connection. ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TDCWelcomeSheet.xib ================================================ Additional options including NickServ password, username, real name, etc. can be edited later by control clicking the connection and opening “Server Properties”. To get started, select a network to connect to using the the control labeled “Destination”, or manually enter the desired server address. ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TVCAlert.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TVCChannelSelectionView.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TVCMainWindow.xib ================================================ Textual is a powerful, yet lightweight, IRC client built exclusively for the Mac. To get started, Textual will walk you through adding your first server. Click the button below to get started. Thank you for trying Textual for the past 30 days. To continue using Textual, you will need to purchase it. ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TVCNotificationConfigurationView.xib ================================================ ================================================ FILE: Sources/App/Resources/User Interface/en.lproj/TXCMainMenu.xib ================================================ CA CQ CQ CQ CQ NSNegateBoolean NSNegateBoolean CA NSNegateBoolean NSNegateBoolean CA ================================================ FILE: Sources/App/Textual App.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 4CCF301015804DCE006FFE21 /* Build Frameworks */ = { isa = PBXAggregateTarget; buildConfigurationList = 4CCF301115804DCE006FFE21 /* Build configuration list for PBXAggregateTarget "Build Frameworks" */; buildPhases = ( 4CCF301715804DD1006FFE21 /* ShellScript */, ); dependencies = ( ); name = "Build Frameworks"; productName = "Build Frameworks"; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 4C06DF2A20EC49A60055D09A /* IRCCommandIndexLocalData.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A020EB676400448776 /* IRCCommandIndexLocalData.plist */; }; 4C06DF2B20EC49A60055D09A /* IRCCommandIndexRemoteData.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C31539F20EB676400448776 /* IRCCommandIndexRemoteData.plist */; }; 4C06DF2C20EC49A60055D09A /* IRCNetworks.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A220EB676400448776 /* IRCNetworks.plist */; }; 4C06DF2F20EC49A60055D09A /* StaticStore.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A420EB676400448776 /* StaticStore.plist */; }; 4C06DF3020EC49A60055D09A /* TemplateLineTypes.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A120EB676400448776 /* TemplateLineTypes.plist */; }; 4C06DF3320EC49A60055D09A /* IRCCommandIndexLocalData.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A020EB676400448776 /* IRCCommandIndexLocalData.plist */; }; 4C06DF3420EC49A60055D09A /* IRCCommandIndexRemoteData.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C31539F20EB676400448776 /* IRCCommandIndexRemoteData.plist */; }; 4C06DF3520EC49A60055D09A /* IRCNetworks.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A220EB676400448776 /* IRCNetworks.plist */; }; 4C06DF3820EC49A60055D09A /* StaticStore.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A420EB676400448776 /* StaticStore.plist */; }; 4C06DF3920EC49A60055D09A /* TemplateLineTypes.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A120EB676400448776 /* TemplateLineTypes.plist */; }; 4C06DF4320EC49B80055D09A /* RemoteLicenseSystemPublicKey.pub in Resources */ = {isa = PBXBuildFile; fileRef = 4C3152EE20EB676400448776 /* RemoteLicenseSystemPublicKey.pub */; }; 4C06DF4420EC49B90055D09A /* RemoteLicenseSystemPublicKey.pub in Resources */ = {isa = PBXBuildFile; fileRef = 4C3152EE20EB676400448776 /* RemoteLicenseSystemPublicKey.pub */; }; 4C06DFC920EC49FA0055D09A /* MainWindowSegmentedControlUserTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537820EB676400448776 /* MainWindowSegmentedControlUserTemplate.pdf */; }; 4C06DFCA20EC49FA0055D09A /* MainWindowSpotlightIconTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537920EB676400448776 /* MainWindowSpotlightIconTemplate.pdf */; }; 4C06DFCB20EC49FA0055D09A /* ErroneousTextFieldValueIndicator.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537B20EB676400448776 /* ErroneousTextFieldValueIndicator.tif */; }; 4C06DFCC20EC49FA0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537E20EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif */; }; 4C06DFCD20EC49FA0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537F20EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif */; }; 4C06DFD020EC49FA0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconActive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538220EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconActive.tif */; }; 4C06DFD120EC49FA0055D09A /* VibrantLightServerListViewPrivateMessageUserIconActive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538320EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconActive.tif */; }; 4C06DFD220EC49FA0055D09A /* VibrantLightServerListViewPrivateMessageUserIconInactive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538420EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconInactive.tif */; }; 4C06DFD320EC49FA0055D09A /* VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538520EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif */; }; 4C06DFD420EC49FA0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconInactive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538620EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconInactive.tif */; }; 4C06DFD520EC49FA0055D09A /* VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538720EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif */; }; 4C06DFE420EC49FB0055D09A /* MainWindowSegmentedControlUserTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537820EB676400448776 /* MainWindowSegmentedControlUserTemplate.pdf */; }; 4C06DFE520EC49FB0055D09A /* MainWindowSpotlightIconTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537920EB676400448776 /* MainWindowSpotlightIconTemplate.pdf */; }; 4C06DFE620EC49FB0055D09A /* ErroneousTextFieldValueIndicator.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537B20EB676400448776 /* ErroneousTextFieldValueIndicator.tif */; }; 4C06DFE720EC49FB0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537E20EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif */; }; 4C06DFE820EC49FB0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537F20EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif */; }; 4C06DFEB20EC49FB0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconActive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538220EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconActive.tif */; }; 4C06DFEC20EC49FB0055D09A /* VibrantLightServerListViewPrivateMessageUserIconActive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538320EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconActive.tif */; }; 4C06DFED20EC49FB0055D09A /* VibrantLightServerListViewPrivateMessageUserIconInactive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538420EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconInactive.tif */; }; 4C06DFEE20EC49FB0055D09A /* VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538520EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif */; }; 4C06DFEF20EC49FB0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconInactive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538620EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconInactive.tif */; }; 4C06DFF020EC49FB0055D09A /* VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31538720EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif */; }; 4C06E02020EC4A060055D09A /* DIRedBadgeRight.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537020EB676400448776 /* DIRedBadgeRight.png */; }; 4C06E02120EC4A060055D09A /* DIRedBadgeLeft.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537120EB676400448776 /* DIRedBadgeLeft.png */; }; 4C06E02220EC4A060055D09A /* DIRedBadgeCenter.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537220EB676400448776 /* DIRedBadgeCenter.png */; }; 4C06E02620EC4A060055D09A /* DIRedBadgeRight.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537020EB676400448776 /* DIRedBadgeRight.png */; }; 4C06E02720EC4A060055D09A /* DIRedBadgeLeft.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537120EB676400448776 /* DIRedBadgeLeft.png */; }; 4C06E02820EC4A060055D09A /* DIRedBadgeCenter.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31537220EB676400448776 /* DIRedBadgeCenter.png */; }; 4C06E03820EC4A0F0055D09A /* DIGreenBadgeLeft.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535F20EB676400448776 /* DIGreenBadgeLeft.png */; }; 4C06E03920EC4A0F0055D09A /* DIGreenBadgeRight.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31536020EB676400448776 /* DIGreenBadgeRight.png */; }; 4C06E03A20EC4A0F0055D09A /* DIGreenBadgeCenter.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31536120EB676400448776 /* DIGreenBadgeCenter.png */; }; 4C06E03E20EC4A100055D09A /* DIGreenBadgeLeft.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535F20EB676400448776 /* DIGreenBadgeLeft.png */; }; 4C06E03F20EC4A100055D09A /* DIGreenBadgeRight.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31536020EB676400448776 /* DIGreenBadgeRight.png */; }; 4C06E04020EC4A100055D09A /* DIGreenBadgeCenter.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31536120EB676400448776 /* DIGreenBadgeCenter.png */; }; 4C06E04C20EC4A2E0055D09A /* applicationIcon.iconset in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535920EB676400448776 /* applicationIcon.iconset */; }; 4C06E04D20EC4A2E0055D09A /* applicationIconBirthday.icns in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535A20EB676400448776 /* applicationIconBirthday.icns */; }; 4C06E04E20EC4A2F0055D09A /* applicationIcon.iconset in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535920EB676400448776 /* applicationIcon.iconset */; }; 4C06E04F20EC4A2F0055D09A /* applicationIconBirthday.icns in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535A20EB676400448776 /* applicationIconBirthday.icns */; }; 4C06E05320EC4A3D0055D09A /* Copyright Information for Images.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535620EB676400448776 /* Copyright Information for Images.txt */; }; 4C06E05420EC4A3E0055D09A /* Copyright Information for Images.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535620EB676400448776 /* Copyright Information for Images.txt */; }; 4C06E05A20EC4A4C0055D09A /* encryptionLockIconLight@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535020EB676400448776 /* encryptionLockIconLight@2x.tif */; }; 4C06E05B20EC4A4C0055D09A /* encryptionLockIconDark.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535120EB676400448776 /* encryptionLockIconDark.tif */; }; 4C06E05C20EC4A4C0055D09A /* encryptionLockIconDark@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535220EB676400448776 /* encryptionLockIconDark@2x.tif */; }; 4C06E05D20EC4A4C0055D09A /* encryptionLockIconLight.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535320EB676400448776 /* encryptionLockIconLight.tif */; }; 4C06E05E20EC4A4C0055D09A /* encryptionLockIconLight@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535020EB676400448776 /* encryptionLockIconLight@2x.tif */; }; 4C06E05F20EC4A4C0055D09A /* encryptionLockIconDark.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535120EB676400448776 /* encryptionLockIconDark.tif */; }; 4C06E06020EC4A4C0055D09A /* encryptionLockIconDark@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535220EB676400448776 /* encryptionLockIconDark@2x.tif */; }; 4C06E06120EC4A4C0055D09A /* encryptionLockIconLight.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31535320EB676400448776 /* encryptionLockIconLight.tif */; }; 4C06E07820EC4A540055D09A /* FormattingColor_8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533D20EB676400448776 /* FormattingColor_8.png */; }; 4C06E07920EC4A540055D09A /* FormattingColor_9.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533E20EB676400448776 /* FormattingColor_9.png */; }; 4C06E07A20EC4A540055D09A /* FormattingColor_11.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533F20EB676400448776 /* FormattingColor_11.png */; }; 4C06E07B20EC4A540055D09A /* FormattingColor_10.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534020EB676400448776 /* FormattingColor_10.png */; }; 4C06E07C20EC4A540055D09A /* FormattingColor_12.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534120EB676400448776 /* FormattingColor_12.png */; }; 4C06E07D20EC4A540055D09A /* FormattingColor_Rainbow.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534220EB676400448776 /* FormattingColor_Rainbow.tif */; }; 4C06E07E20EC4A540055D09A /* FormattingColor_13.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534320EB676400448776 /* FormattingColor_13.png */; }; 4C06E07F20EC4A540055D09A /* FormattingColor_14.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534420EB676400448776 /* FormattingColor_14.png */; }; 4C06E08020EC4A540055D09A /* FormattingColor_15.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534520EB676400448776 /* FormattingColor_15.png */; }; 4C06E08120EC4A540055D09A /* FormattingColor_7.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534620EB676400448776 /* FormattingColor_7.png */; }; 4C06E08220EC4A540055D09A /* FormattingColor_6.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534720EB676400448776 /* FormattingColor_6.png */; }; 4C06E08320EC4A540055D09A /* FormattingColor_4.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534820EB676400448776 /* FormattingColor_4.png */; }; 4C06E08420EC4A540055D09A /* FormattingColor_5.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534920EB676400448776 /* FormattingColor_5.png */; }; 4C06E08520EC4A540055D09A /* FormattingColor_1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534A20EB676400448776 /* FormattingColor_1.png */; }; 4C06E08620EC4A540055D09A /* FormattingColor_Rainbow@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534B20EB676400448776 /* FormattingColor_Rainbow@2x.tif */; }; 4C06E08720EC4A540055D09A /* FormattingColor_0.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534C20EB676400448776 /* FormattingColor_0.png */; }; 4C06E08820EC4A540055D09A /* FormattingColor_2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534D20EB676400448776 /* FormattingColor_2.png */; }; 4C06E08920EC4A540055D09A /* FormattingColor_3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534E20EB676400448776 /* FormattingColor_3.png */; }; 4C06E08A20EC4A540055D09A /* FormattingColor_8.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533D20EB676400448776 /* FormattingColor_8.png */; }; 4C06E08B20EC4A540055D09A /* FormattingColor_9.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533E20EB676400448776 /* FormattingColor_9.png */; }; 4C06E08C20EC4A540055D09A /* FormattingColor_11.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533F20EB676400448776 /* FormattingColor_11.png */; }; 4C06E08D20EC4A540055D09A /* FormattingColor_10.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534020EB676400448776 /* FormattingColor_10.png */; }; 4C06E08E20EC4A540055D09A /* FormattingColor_12.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534120EB676400448776 /* FormattingColor_12.png */; }; 4C06E08F20EC4A540055D09A /* FormattingColor_Rainbow.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534220EB676400448776 /* FormattingColor_Rainbow.tif */; }; 4C06E09020EC4A540055D09A /* FormattingColor_13.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534320EB676400448776 /* FormattingColor_13.png */; }; 4C06E09120EC4A540055D09A /* FormattingColor_14.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534420EB676400448776 /* FormattingColor_14.png */; }; 4C06E09220EC4A540055D09A /* FormattingColor_15.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534520EB676400448776 /* FormattingColor_15.png */; }; 4C06E09320EC4A540055D09A /* FormattingColor_7.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534620EB676400448776 /* FormattingColor_7.png */; }; 4C06E09420EC4A540055D09A /* FormattingColor_6.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534720EB676400448776 /* FormattingColor_6.png */; }; 4C06E09520EC4A540055D09A /* FormattingColor_4.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534820EB676400448776 /* FormattingColor_4.png */; }; 4C06E09620EC4A540055D09A /* FormattingColor_5.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534920EB676400448776 /* FormattingColor_5.png */; }; 4C06E09720EC4A540055D09A /* FormattingColor_1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534A20EB676400448776 /* FormattingColor_1.png */; }; 4C06E09820EC4A540055D09A /* FormattingColor_Rainbow@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534B20EB676400448776 /* FormattingColor_Rainbow@2x.tif */; }; 4C06E09920EC4A540055D09A /* FormattingColor_0.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534C20EB676400448776 /* FormattingColor_0.png */; }; 4C06E09A20EC4A540055D09A /* FormattingColor_2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534D20EB676400448776 /* FormattingColor_2.png */; }; 4C06E09B20EC4A540055D09A /* FormattingColor_3.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31534E20EB676400448776 /* FormattingColor_3.png */; }; 4C06E0B520EC4A5A0055D09A /* channelRoomStatusIconLightActive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533120EB676400448776 /* channelRoomStatusIconLightActive@2x.tif */; }; 4C06E0B620EC4A5A0055D09A /* channelRoomStatusIconDarkActive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533220EB676400448776 /* channelRoomStatusIconDarkActive@2x.tif */; }; 4C06E0B720EC4A5A0055D09A /* channelRoomStatusIconLightInactive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533320EB676400448776 /* channelRoomStatusIconLightInactive.tif */; }; 4C06E0B820EC4A5A0055D09A /* channelRoomStatusIconDarkInactive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533420EB676400448776 /* channelRoomStatusIconDarkInactive@2x.tif */; }; 4C06E0BA20EC4A5A0055D09A /* channelRoomStatusIconDarkActive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533620EB676400448776 /* channelRoomStatusIconDarkActive.tif */; }; 4C06E0BB20EC4A5A0055D09A /* channelRoomStatusIconLightActive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533720EB676400448776 /* channelRoomStatusIconLightActive.tif */; }; 4C06E0BD20EC4A5A0055D09A /* channelRoomStatusIconDarkInactive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533920EB676400448776 /* channelRoomStatusIconDarkInactive.tif */; }; 4C06E0BF20EC4A5A0055D09A /* channelRoomStatusIconLightInactive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533B20EB676400448776 /* channelRoomStatusIconLightInactive@2x.tif */; }; 4C06E0C720EC4A5B0055D09A /* channelRoomStatusIconLightActive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533120EB676400448776 /* channelRoomStatusIconLightActive@2x.tif */; }; 4C06E0C820EC4A5B0055D09A /* channelRoomStatusIconDarkActive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533220EB676400448776 /* channelRoomStatusIconDarkActive@2x.tif */; }; 4C06E0C920EC4A5B0055D09A /* channelRoomStatusIconLightInactive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533320EB676400448776 /* channelRoomStatusIconLightInactive.tif */; }; 4C06E0CA20EC4A5B0055D09A /* channelRoomStatusIconDarkInactive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533420EB676400448776 /* channelRoomStatusIconDarkInactive@2x.tif */; }; 4C06E0CC20EC4A5B0055D09A /* channelRoomStatusIconDarkActive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533620EB676400448776 /* channelRoomStatusIconDarkActive.tif */; }; 4C06E0CD20EC4A5B0055D09A /* channelRoomStatusIconLightActive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533720EB676400448776 /* channelRoomStatusIconLightActive.tif */; }; 4C06E0CF20EC4A5B0055D09A /* channelRoomStatusIconDarkInactive.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533920EB676400448776 /* channelRoomStatusIconDarkInactive.tif */; }; 4C06E0D120EC4A5B0055D09A /* channelRoomStatusIconLightInactive@2x.tif in Resources */ = {isa = PBXBuildFile; fileRef = 4C31533B20EB676400448776 /* channelRoomStatusIconLightInactive@2x.tif */; }; 4C06E10620EC4A630055D09A /* TPWTB_Advanced@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531920EB676400448776 /* TPWTB_Advanced@2x.png */; }; 4C06E10720EC4A630055D09A /* TPWTB_Highlights.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531A20EB676400448776 /* TPWTB_Highlights.png */; }; 4C06E10820EC4A630055D09A /* TPWTB_Highlights@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531B20EB676400448776 /* TPWTB_Highlights@2x.png */; }; 4C06E10920EC4A630055D09A /* TPWTB_Addons.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531C20EB676400448776 /* TPWTB_Addons.png */; }; 4C06E10A20EC4A630055D09A /* TPWTB_Advanced.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531D20EB676400448776 /* TPWTB_Advanced.png */; }; 4C06E10B20EC4A630055D09A /* TPWTB_Controls.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531E20EB676400448776 /* TPWTB_Controls.png */; }; 4C06E10C20EC4A630055D09A /* TPWTB_Style@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531F20EB676400448776 /* TPWTB_Style@2x.png */; }; 4C06E10D20EC4A630055D09A /* TPWTB_General@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532020EB676400448776 /* TPWTB_General@2x.png */; }; 4C06E10E20EC4A630055D09A /* TPWTB_Addons@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532120EB676400448776 /* TPWTB_Addons@2x.png */; }; 4C06E10F20EC4A630055D09A /* TPWTB_Notifications@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532220EB676400448776 /* TPWTB_Notifications@2x.png */; }; 4C06E11020EC4A630055D09A /* TPWTB_Controls@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532320EB676400448776 /* TPWTB_Controls@2x.png */; }; 4C06E11120EC4A630055D09A /* TPWTB_Interface.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532420EB676400448776 /* TPWTB_Interface.png */; }; 4C06E11220EC4A630055D09A /* TPWTB_Interface@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532520EB676400448776 /* TPWTB_Interface@2x.png */; }; 4C06E11320EC4A630055D09A /* TPWTB_Notifications.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532620EB676400448776 /* TPWTB_Notifications.png */; }; 4C06E11420EC4A630055D09A /* TPWTB_Style.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532720EB676400448776 /* TPWTB_Style.png */; }; 4C06E11520EC4A630055D09A /* TPWTB_General.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532820EB676400448776 /* TPWTB_General.png */; }; 4C06E11620EC4A630055D09A /* TPWTB_Advanced@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531920EB676400448776 /* TPWTB_Advanced@2x.png */; }; 4C06E11720EC4A630055D09A /* TPWTB_Highlights.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531A20EB676400448776 /* TPWTB_Highlights.png */; }; 4C06E11820EC4A630055D09A /* TPWTB_Highlights@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531B20EB676400448776 /* TPWTB_Highlights@2x.png */; }; 4C06E11920EC4A630055D09A /* TPWTB_Addons.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531C20EB676400448776 /* TPWTB_Addons.png */; }; 4C06E11A20EC4A630055D09A /* TPWTB_Advanced.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531D20EB676400448776 /* TPWTB_Advanced.png */; }; 4C06E11B20EC4A630055D09A /* TPWTB_Controls.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531E20EB676400448776 /* TPWTB_Controls.png */; }; 4C06E11C20EC4A630055D09A /* TPWTB_Style@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31531F20EB676400448776 /* TPWTB_Style@2x.png */; }; 4C06E11D20EC4A630055D09A /* TPWTB_General@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532020EB676400448776 /* TPWTB_General@2x.png */; }; 4C06E11E20EC4A630055D09A /* TPWTB_Addons@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532120EB676400448776 /* TPWTB_Addons@2x.png */; }; 4C06E11F20EC4A630055D09A /* TPWTB_Notifications@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532220EB676400448776 /* TPWTB_Notifications@2x.png */; }; 4C06E12020EC4A630055D09A /* TPWTB_Controls@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532320EB676400448776 /* TPWTB_Controls@2x.png */; }; 4C06E12120EC4A630055D09A /* TPWTB_Interface.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532420EB676400448776 /* TPWTB_Interface.png */; }; 4C06E12220EC4A630055D09A /* TPWTB_Interface@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532520EB676400448776 /* TPWTB_Interface@2x.png */; }; 4C06E12320EC4A630055D09A /* TPWTB_Notifications.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532620EB676400448776 /* TPWTB_Notifications.png */; }; 4C06E12420EC4A630055D09A /* TPWTB_Style.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532720EB676400448776 /* TPWTB_Style.png */; }; 4C06E12520EC4A630055D09A /* TPWTB_General.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C31532820EB676400448776 /* TPWTB_General.png */; }; 4C06E18B20EC4AC40055D09A /* TDCServerPropertiesSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E13920EC4AC40055D09A /* TDCServerPropertiesSheet.xib */; }; 4C06E18C20EC4AC40055D09A /* TDCServerPropertiesSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E13920EC4AC40055D09A /* TDCServerPropertiesSheet.xib */; }; 4C06E19320EC4AC40055D09A /* TDCServerHighlightListSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E13D20EC4AC40055D09A /* TDCServerHighlightListSheet.xib */; }; 4C06E19420EC4AC40055D09A /* TDCServerHighlightListSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E13D20EC4AC40055D09A /* TDCServerHighlightListSheet.xib */; }; 4C06E19720EC4AC40055D09A /* TDCServerEndpointListSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E13F20EC4AC40055D09A /* TDCServerEndpointListSheet.xib */; }; 4C06E19820EC4AC40055D09A /* TDCServerEndpointListSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E13F20EC4AC40055D09A /* TDCServerEndpointListSheet.xib */; }; 4C06E19B20EC4AC40055D09A /* TDCChannelModifyModesSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14120EC4AC40055D09A /* TDCChannelModifyModesSheet.xib */; }; 4C06E19C20EC4AC40055D09A /* TDCChannelModifyModesSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14120EC4AC40055D09A /* TDCChannelModifyModesSheet.xib */; }; 4C06E19F20EC4AC40055D09A /* TDCChannelSpotlightController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14320EC4AC40055D09A /* TDCChannelSpotlightController.xib */; }; 4C06E1A020EC4AC40055D09A /* TDCChannelSpotlightController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14320EC4AC40055D09A /* TDCChannelSpotlightController.xib */; }; 4C06E1A320EC4AC40055D09A /* TDCFileTransferDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14520EC4AC40055D09A /* TDCFileTransferDialog.xib */; }; 4C06E1A420EC4AC40055D09A /* TDCFileTransferDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14520EC4AC40055D09A /* TDCFileTransferDialog.xib */; }; 4C06E1A720EC4AC40055D09A /* TDCServerChannelListDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14720EC4AC40055D09A /* TDCServerChannelListDialog.xib */; }; 4C06E1A820EC4AC40055D09A /* TDCServerChannelListDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14720EC4AC40055D09A /* TDCServerChannelListDialog.xib */; }; 4C06E1AB20EC4AC40055D09A /* TDCChannelBanListSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14920EC4AC40055D09A /* TDCChannelBanListSheet.xib */; }; 4C06E1AC20EC4AC40055D09A /* TDCChannelBanListSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14920EC4AC40055D09A /* TDCChannelBanListSheet.xib */; }; 4C06E1AF20EC4AC40055D09A /* TDCChannelPropertiesSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14B20EC4AC40055D09A /* TDCChannelPropertiesSheet.xib */; }; 4C06E1B020EC4AC40055D09A /* TDCChannelPropertiesSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14B20EC4AC40055D09A /* TDCChannelPropertiesSheet.xib */; }; 4C06E1B320EC4AC40055D09A /* TDCLicenseManagerMigrateAppStoreSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14D20EC4AC40055D09A /* TDCLicenseManagerMigrateAppStoreSheet.xib */; }; 4C06E1B420EC4AC40055D09A /* TDCLicenseManagerMigrateAppStoreSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E14D20EC4AC40055D09A /* TDCLicenseManagerMigrateAppStoreSheet.xib */; }; 4C06E1BB20EC4AC40055D09A /* TDCLicenseUpgradeEligibilitySheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15120EC4AC40055D09A /* TDCLicenseUpgradeEligibilitySheet.xib */; }; 4C06E1BC20EC4AC40055D09A /* TDCLicenseUpgradeEligibilitySheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15120EC4AC40055D09A /* TDCLicenseUpgradeEligibilitySheet.xib */; }; 4C06E1C320EC4AC40055D09A /* TVCChannelSelectionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15520EC4AC40055D09A /* TVCChannelSelectionView.xib */; }; 4C06E1C420EC4AC40055D09A /* TVCChannelSelectionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15520EC4AC40055D09A /* TVCChannelSelectionView.xib */; }; 4C06E1C720EC4AC40055D09A /* TXCMainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15720EC4AC40055D09A /* TXCMainMenu.xib */; }; 4C06E1C820EC4AC40055D09A /* TXCMainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15720EC4AC40055D09A /* TXCMainMenu.xib */; }; 4C06E1CB20EC4AC40055D09A /* TVCNotificationConfigurationView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15920EC4AC40055D09A /* TVCNotificationConfigurationView.xib */; }; 4C06E1CC20EC4AC40055D09A /* TVCNotificationConfigurationView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15920EC4AC40055D09A /* TVCNotificationConfigurationView.xib */; }; 4C06E1CF20EC4AC40055D09A /* TDCLicenseUpgradeActivateSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15B20EC4AC40055D09A /* TDCLicenseUpgradeActivateSheet.xib */; }; 4C06E1D020EC4AC40055D09A /* TDCLicenseUpgradeActivateSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15B20EC4AC40055D09A /* TDCLicenseUpgradeActivateSheet.xib */; }; 4C06E1D320EC4AC40055D09A /* TDCPreferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15D20EC4AC40055D09A /* TDCPreferences.xib */; }; 4C06E1D420EC4AC40055D09A /* TDCPreferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15D20EC4AC40055D09A /* TDCPreferences.xib */; }; 4C06E1D720EC4AC40055D09A /* TDCChannelInviteSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15F20EC4AC40055D09A /* TDCChannelInviteSheet.xib */; }; 4C06E1D820EC4AC40055D09A /* TDCChannelInviteSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E15F20EC4AC40055D09A /* TDCChannelInviteSheet.xib */; }; 4C06E1DB20EC4AC40055D09A /* TDCNicknameColorSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16120EC4AC40055D09A /* TDCNicknameColorSheet.xib */; }; 4C06E1DC20EC4AC40055D09A /* TDCNicknameColorSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16120EC4AC40055D09A /* TDCNicknameColorSheet.xib */; }; 4C06E1DF20EC4AC40055D09A /* TDCWelcomeSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16320EC4AC40055D09A /* TDCWelcomeSheet.xib */; }; 4C06E1E020EC4AC40055D09A /* TDCWelcomeSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16320EC4AC40055D09A /* TDCWelcomeSheet.xib */; }; 4C06E1E320EC4AC40055D09A /* TDCProgressIndicatorSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16520EC4AC40055D09A /* TDCProgressIndicatorSheet.xib */; }; 4C06E1E420EC4AC40055D09A /* TDCProgressIndicatorSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16520EC4AC40055D09A /* TDCProgressIndicatorSheet.xib */; }; 4C06E1E720EC4AC50055D09A /* TDCAddressBookSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16720EC4AC40055D09A /* TDCAddressBookSheet.xib */; }; 4C06E1E820EC4AC50055D09A /* TDCAddressBookSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16720EC4AC40055D09A /* TDCAddressBookSheet.xib */; }; 4C06E1EB20EC4AC50055D09A /* TVCAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16920EC4AC40055D09A /* TVCAlert.xib */; }; 4C06E1EC20EC4AC50055D09A /* TVCAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16920EC4AC40055D09A /* TVCAlert.xib */; }; 4C06E1EF20EC4AC50055D09A /* TDCLicenseUpgradeDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16B20EC4AC40055D09A /* TDCLicenseUpgradeDialog.xib */; }; 4C06E1F020EC4AC50055D09A /* TDCLicenseUpgradeDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16B20EC4AC40055D09A /* TDCLicenseUpgradeDialog.xib */; }; 4C06E1F320EC4AC50055D09A /* TDCServerChangeNicknameSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16D20EC4AC40055D09A /* TDCServerChangeNicknameSheet.xib */; }; 4C06E1F420EC4AC50055D09A /* TDCServerChangeNicknameSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16D20EC4AC40055D09A /* TDCServerChangeNicknameSheet.xib */; }; 4C06E1F720EC4AC50055D09A /* TDCChannelModifyTopicSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16F20EC4AC40055D09A /* TDCChannelModifyTopicSheet.xib */; }; 4C06E1F820EC4AC50055D09A /* TDCChannelModifyTopicSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E16F20EC4AC40055D09A /* TDCChannelModifyTopicSheet.xib */; }; 4C06E1FB20EC4AC50055D09A /* TDCAboutDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17120EC4AC40055D09A /* TDCAboutDialog.xib */; }; 4C06E1FC20EC4AC50055D09A /* TDCAboutDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17120EC4AC40055D09A /* TDCAboutDialog.xib */; }; 4C06E1FF20EC4AC50055D09A /* TDCHighlightEntrySheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17320EC4AC40055D09A /* TDCHighlightEntrySheet.xib */; }; 4C06E20020EC4AC50055D09A /* TDCHighlightEntrySheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17320EC4AC40055D09A /* TDCHighlightEntrySheet.xib */; }; 4C06E20320EC4AC50055D09A /* TVCMainWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17520EC4AC40055D09A /* TVCMainWindow.xib */; }; 4C06E20420EC4AC50055D09A /* TVCMainWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17520EC4AC40055D09A /* TVCMainWindow.xib */; }; 4C06E20720EC4AC50055D09A /* TDCLicenseManagerDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17720EC4AC40055D09A /* TDCLicenseManagerDialog.xib */; }; 4C06E20820EC4AC50055D09A /* TDCLicenseManagerDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17720EC4AC40055D09A /* TDCLicenseManagerDialog.xib */; }; 4C06E20B20EC4AC50055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17920EC4AC40055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.xib */; }; 4C06E20C20EC4AC50055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17920EC4AC40055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.xib */; }; 4C06E20F20EC4AC50055D09A /* TVCMemberListAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17C20EC4AC40055D09A /* TVCMemberListAppearance.plist */; }; 4C06E21020EC4AC50055D09A /* TVCMemberListAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E17C20EC4AC40055D09A /* TVCMemberListAppearance.plist */; }; 4C06E22320EC4AC50055D09A /* TDCChannelSpotlightAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E18120EC4AC40055D09A /* TDCChannelSpotlightAppearance.plist */; }; 4C06E22420EC4AC50055D09A /* TDCChannelSpotlightAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E18120EC4AC40055D09A /* TDCChannelSpotlightAppearance.plist */; }; 4C06E22720EC4AC50055D09A /* TVCMainWindowAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E18220EC4AC40055D09A /* TVCMainWindowAppearance.plist */; }; 4C06E22820EC4AC50055D09A /* TVCMainWindowAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E18220EC4AC40055D09A /* TVCMainWindowAppearance.plist */; }; 4C06E22B20EC4AC50055D09A /* TVCMainWindowTextViewAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E18320EC4AC40055D09A /* TVCMainWindowTextViewAppearance.plist */; }; 4C06E22C20EC4AC50055D09A /* TVCMainWindowTextViewAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E18320EC4AC40055D09A /* TVCMainWindowTextViewAppearance.plist */; }; 4C06E22F20EC4AC50055D09A /* TVCServerListAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E18420EC4AC40055D09A /* TVCServerListAppearance.plist */; }; 4C06E23020EC4AC50055D09A /* TVCServerListAppearance.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E18420EC4AC40055D09A /* TVCServerListAppearance.plist */; }; 4C06E24920EC4B970055D09A /* IRCConnectionConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E24720EC4B970055D09A /* IRCConnectionConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E24A20EC4B970055D09A /* IRCConnectionConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E24720EC4B970055D09A /* IRCConnectionConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E25D20EC4BBD0055D09A /* IRCConnectionConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E25B20EC4BBD0055D09A /* IRCConnectionConfigInternal.h */; }; 4C06E25E20EC4BBD0055D09A /* IRCConnectionConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E25B20EC4BBD0055D09A /* IRCConnectionConfigInternal.h */; }; 4C06E26420EC4C560055D09A /* TPCPreferencesPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E26020EC4C560055D09A /* TPCPreferencesPrivate.h */; }; 4C06E26520EC4C560055D09A /* TPCPreferencesPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E26020EC4C560055D09A /* TPCPreferencesPrivate.h */; }; 4C06E26820EC4C560055D09A /* TPCPreferencesUserDefaultsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E26120EC4C560055D09A /* TPCPreferencesUserDefaultsPrivate.h */; }; 4C06E26920EC4C560055D09A /* TPCPreferencesUserDefaultsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E26120EC4C560055D09A /* TPCPreferencesUserDefaultsPrivate.h */; }; 4C06E26C20EC4C560055D09A /* NSObjectHelperPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E26220EC4C560055D09A /* NSObjectHelperPrivate.h */; }; 4C06E26D20EC4C560055D09A /* NSObjectHelperPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E26220EC4C560055D09A /* NSObjectHelperPrivate.h */; }; 4C06E27120EC4E350055D09A /* StaticDefinitions.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E26F20EC4E350055D09A /* StaticDefinitions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E27220EC4E350055D09A /* StaticDefinitions.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E26F20EC4E350055D09A /* StaticDefinitions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E27920EC4E870055D09A /* TLOTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E27420EC4E850055D09A /* TLOTimer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E27A20EC4E870055D09A /* TLOTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E27420EC4E850055D09A /* TLOTimer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E27D20EC4E870055D09A /* TPCPreferencesUserDefaults.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E27520EC4E850055D09A /* TPCPreferencesUserDefaults.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E27E20EC4E870055D09A /* TPCPreferencesUserDefaults.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E27520EC4E850055D09A /* TPCPreferencesUserDefaults.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E28120EC4E870055D09A /* TPCPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E27620EC4E860055D09A /* TPCPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E28220EC4E870055D09A /* TPCPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E27620EC4E860055D09A /* TPCPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E28520EC4E870055D09A /* TLOLocalization.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E27720EC4E860055D09A /* TLOLocalization.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E28620EC4E870055D09A /* TLOLocalization.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E27720EC4E860055D09A /* TLOLocalization.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C06E2DD20EC4FF00055D09A /* Prompts.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2A720EC4FF00055D09A /* Prompts.strings */; }; 4C06E2DE20EC4FF00055D09A /* Prompts.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2A720EC4FF00055D09A /* Prompts.strings */; }; 4C06E2E120EC4FF00055D09A /* OffTheRecord.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2A920EC4FF00055D09A /* OffTheRecord.strings */; }; 4C06E2E220EC4FF00055D09A /* OffTheRecord.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2A920EC4FF00055D09A /* OffTheRecord.strings */; }; 4C06E2E520EC4FF00055D09A /* CommonErrors.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2AB20EC4FF00055D09A /* CommonErrors.strings */; }; 4C06E2E620EC4FF00055D09A /* CommonErrors.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2AB20EC4FF00055D09A /* CommonErrors.strings */; }; 4C06E2ED20EC4FF00055D09A /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2AF20EC4FF00055D09A /* BasicLanguage.strings */; }; 4C06E2EE20EC4FF00055D09A /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2AF20EC4FF00055D09A /* BasicLanguage.strings */; }; 4C06E2F120EC4FF00055D09A /* Accessibility.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B120EC4FF00055D09A /* Accessibility.strings */; }; 4C06E2F220EC4FF00055D09A /* Accessibility.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B120EC4FF00055D09A /* Accessibility.strings */; }; 4C06E2F520EC4FF00055D09A /* TDCChannelPropertiesSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B320EC4FF00055D09A /* TDCChannelPropertiesSheet.strings */; }; 4C06E2F620EC4FF00055D09A /* TDCChannelPropertiesSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B320EC4FF00055D09A /* TDCChannelPropertiesSheet.strings */; }; 4C06E2F920EC4FF00055D09A /* TDCChannelBanListSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B520EC4FF00055D09A /* TDCChannelBanListSheet.strings */; }; 4C06E2FA20EC4FF00055D09A /* TDCChannelBanListSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B520EC4FF00055D09A /* TDCChannelBanListSheet.strings */; }; 4C06E2FD20EC4FF00055D09A /* TDCPreferencesController.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B720EC4FF00055D09A /* TDCPreferencesController.strings */; }; 4C06E2FE20EC4FF00055D09A /* TDCPreferencesController.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B720EC4FF00055D09A /* TDCPreferencesController.strings */; }; 4C06E30120EC4FF00055D09A /* TDCFileTransferDialog.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B920EC4FF00055D09A /* TDCFileTransferDialog.strings */; }; 4C06E30220EC4FF00055D09A /* TDCFileTransferDialog.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2B920EC4FF00055D09A /* TDCFileTransferDialog.strings */; }; 4C06E30520EC4FF00055D09A /* TDCChannelInviteSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2BB20EC4FF00055D09A /* TDCChannelInviteSheet.strings */; }; 4C06E30620EC4FF00055D09A /* TDCChannelInviteSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2BB20EC4FF00055D09A /* TDCChannelInviteSheet.strings */; }; 4C06E30920EC4FF00055D09A /* TDCAboutDialog.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2BD20EC4FF00055D09A /* TDCAboutDialog.strings */; }; 4C06E30A20EC4FF00055D09A /* TDCAboutDialog.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2BD20EC4FF00055D09A /* TDCAboutDialog.strings */; }; 4C06E30D20EC4FF00055D09A /* TDCServerPropertiesSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2BF20EC4FF00055D09A /* TDCServerPropertiesSheet.strings */; }; 4C06E30E20EC4FF00055D09A /* TDCServerPropertiesSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2BF20EC4FF00055D09A /* TDCServerPropertiesSheet.strings */; }; 4C06E31120EC4FF00055D09A /* IRC.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C120EC4FF00055D09A /* IRC.strings */; }; 4C06E31220EC4FF00055D09A /* IRC.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C120EC4FF00055D09A /* IRC.strings */; }; 4C06E31520EC4FF00055D09A /* TLOLicenseManager.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C320EC4FF00055D09A /* TLOLicenseManager.strings */; }; 4C06E31620EC4FF00055D09A /* TLOLicenseManager.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C320EC4FF00055D09A /* TLOLicenseManager.strings */; }; 4C06E31920EC4FF00055D09A /* TDCChannelModifyModesSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C520EC4FF00055D09A /* TDCChannelModifyModesSheet.strings */; }; 4C06E31A20EC4FF00055D09A /* TDCChannelModifyModesSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C520EC4FF00055D09A /* TDCChannelModifyModesSheet.strings */; }; 4C06E31D20EC4FF00055D09A /* TVCMainWindow.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C720EC4FF00055D09A /* TVCMainWindow.strings */; }; 4C06E31E20EC4FF00055D09A /* TVCMainWindow.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C720EC4FF00055D09A /* TVCMainWindow.strings */; }; 4C06E32120EC4FF00055D09A /* Notifications.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C920EC4FF00055D09A /* Notifications.strings */; }; 4C06E32220EC4FF00055D09A /* Notifications.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2C920EC4FF00055D09A /* Notifications.strings */; }; 4C06E32920EC4FF00055D09A /* TVCNotificationConfigurationView.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2CD20EC4FF00055D09A /* TVCNotificationConfigurationView.strings */; }; 4C06E32A20EC4FF00055D09A /* TVCNotificationConfigurationView.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2CD20EC4FF00055D09A /* TVCNotificationConfigurationView.strings */; }; 4C06E32D20EC4FF00055D09A /* TDCServerEndpointListSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2CF20EC4FF00055D09A /* TDCServerEndpointListSheet.strings */; }; 4C06E32E20EC4FF00055D09A /* TDCServerEndpointListSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2CF20EC4FF00055D09A /* TDCServerEndpointListSheet.strings */; }; 4C06E33120EC4FF00055D09A /* TDCServerChannelListDialog.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D120EC4FF00055D09A /* TDCServerChannelListDialog.strings */; }; 4C06E33220EC4FF00055D09A /* TDCServerChannelListDialog.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D120EC4FF00055D09A /* TDCServerChannelListDialog.strings */; }; 4C06E33520EC4FF00055D09A /* TDCChannelSpotlightController.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D320EC4FF00055D09A /* TDCChannelSpotlightController.strings */; }; 4C06E33620EC4FF00055D09A /* TDCChannelSpotlightController.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D320EC4FF00055D09A /* TDCChannelSpotlightController.strings */; }; 4C06E33920EC4FF00055D09A /* TDCAddressBookSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D520EC4FF00055D09A /* TDCAddressBookSheet.strings */; }; 4C06E33A20EC4FF00055D09A /* TDCAddressBookSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D520EC4FF00055D09A /* TDCAddressBookSheet.strings */; }; 4C06E33D20EC4FF00055D09A /* TDCLicenseUpgradeEligibilitySheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D720EC4FF00055D09A /* TDCLicenseUpgradeEligibilitySheet.strings */; }; 4C06E33E20EC4FF00055D09A /* TDCLicenseUpgradeEligibilitySheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D720EC4FF00055D09A /* TDCLicenseUpgradeEligibilitySheet.strings */; }; 4C06E34120EC4FF00055D09A /* TDCChannelModifyTopicSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D920EC4FF00055D09A /* TDCChannelModifyTopicSheet.strings */; }; 4C06E34220EC4FF00055D09A /* TDCChannelModifyTopicSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C06E2D920EC4FF00055D09A /* TDCChannelModifyTopicSheet.strings */; }; 4C06E3B120EC51380055D09A /* GCDAsyncSocketExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E36B20EC51370055D09A /* GCDAsyncSocketExtensions.h */; }; 4C06E3B220EC51380055D09A /* GCDAsyncSocketExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E36B20EC51370055D09A /* GCDAsyncSocketExtensions.h */; }; 4C06E3B520EC51380055D09A /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E36C20EC51370055D09A /* GCDAsyncSocket.h */; }; 4C06E3B620EC51380055D09A /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E36C20EC51370055D09A /* GCDAsyncSocket.h */; }; 4C06E3C920EC51380055D09A /* ICLPayloadInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E37220EC51370055D09A /* ICLPayloadInternal.h */; }; 4C06E3CA20EC51380055D09A /* ICLPayloadInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E37220EC51370055D09A /* ICLPayloadInternal.h */; }; 4C06E3D520EC51380055D09A /* ICLPayload.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E37520EC51370055D09A /* ICLPayload.h */; }; 4C06E3D620EC51380055D09A /* ICLPayload.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E37520EC51370055D09A /* ICLPayload.h */; }; 4C06E3F520EC51380055D09A /* RCMConnectionManagerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E37F20EC51370055D09A /* RCMConnectionManagerProtocol.h */; }; 4C06E3F620EC51380055D09A /* RCMConnectionManagerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E37F20EC51370055D09A /* RCMConnectionManagerProtocol.h */; }; 4C06E3F920EC51380055D09A /* ICLInlineContentProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E38020EC51370055D09A /* ICLInlineContentProtocol.h */; }; 4C06E3FA20EC51380055D09A /* ICLInlineContentProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E38020EC51370055D09A /* ICLInlineContentProtocol.h */; }; 4C06E3FD20EC51380055D09A /* HLSHistoricLogProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E38120EC51370055D09A /* HLSHistoricLogProtocol.h */; }; 4C06E3FE20EC51380055D09A /* HLSHistoricLogProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E38120EC51370055D09A /* HLSHistoricLogProtocol.h */; }; 4C06E41220EC52240055D09A /* TVCLogLineXPCPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E41020EC52240055D09A /* TVCLogLineXPCPrivate.h */; }; 4C06E41320EC52240055D09A /* TVCLogLineXPCPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E41020EC52240055D09A /* TVCLogLineXPCPrivate.h */; }; 4C06E41720EC52D60055D09A /* ICLPayloadLocalPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E41520EC52D50055D09A /* ICLPayloadLocalPrivate.h */; }; 4C06E41820EC52D60055D09A /* ICLPayloadLocalPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C06E41520EC52D50055D09A /* ICLPayloadLocalPrivate.h */; }; 4C06E42920EC53620055D09A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CCE521A18C7BE0600D49601 /* AudioToolbox.framework */; }; 4C06E42A20EC53620055D09A /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C46CCBC1580469E00846B64 /* Cocoa.framework */; }; 4C06E42B20EC53620055D09A /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CA8EC621B63C2970087BF72 /* CoreServices.framework */; }; 4C06E42D20EC53620055D09A /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C10C00D1F37E59B0004C624 /* IOKit.framework */; }; 4C06E42F20EC53620055D09A /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF4D6B619837C1700FB5AF0 /* QuartzCore.framework */; }; 4C06E43020EC53620055D09A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C46CCC01580469E00846B64 /* Security.framework */; }; 4C06E43320EC53620055D09A /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C46CCC11580469E00846B64 /* SystemConfiguration.framework */; }; 4C06E43420EC53620055D09A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C46CCC21580469E00846B64 /* WebKit.framework */; }; 4C06E43520EC539E0055D09A /* AutoHyperlinks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF76A371A91153A0088BF9A /* AutoHyperlinks.framework */; }; 4C06E43620EC539E0055D09A /* AutoHyperlinks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF76A371A91153A0088BF9A /* AutoHyperlinks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4C06E43720EC539E0055D09A /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF76A391A91153A0088BF9A /* CocoaExtensions.framework */; }; 4C06E43820EC539E0055D09A /* CocoaExtensions.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF76A391A91153A0088BF9A /* CocoaExtensions.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4C06E43920EC539E0055D09A /* EncryptionKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8F2F5D1AAE6467007821CC /* EncryptionKit.framework */; }; 4C06E43A20EC539E0055D09A /* EncryptionKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8F2F5D1AAE6467007821CC /* EncryptionKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4C06E43B20EC539E0055D09A /* GRMustache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC4A7371F8A1404008FF15F /* GRMustache.framework */; }; 4C06E43C20EC539E0055D09A /* GRMustache.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC4A7371F8A1404008FF15F /* GRMustache.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4C06E44120EC539E0055D09A /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C24DC751A93C9CE0098D1BE /* Sparkle.framework */; }; 4C06E44220EC539E0055D09A /* Sparkle.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4C24DC751A93C9CE0098D1BE /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4C06E47320EC541C0055D09A /* AutoHyperlinks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF76A371A91153A0088BF9A /* AutoHyperlinks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4C06E47520EC541C0055D09A /* CocoaExtensions.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF76A391A91153A0088BF9A /* CocoaExtensions.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4C06E47720EC541C0055D09A /* EncryptionKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8F2F5D1AAE6467007821CC /* EncryptionKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4C06E47920EC541C0055D09A /* GRMustache.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC4A7371F8A1404008FF15F /* GRMustache.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4C06E47F20EC541C0055D09A /* Sparkle.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4C24DC751A93C9CE0098D1BE /* Sparkle.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4C06E48120EC54260055D09A /* AutoHyperlinks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF76A371A91153A0088BF9A /* AutoHyperlinks.framework */; }; 4C06E48220EC54260055D09A /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF76A391A91153A0088BF9A /* CocoaExtensions.framework */; }; 4C06E48320EC54260055D09A /* EncryptionKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8F2F5D1AAE6467007821CC /* EncryptionKit.framework */; }; 4C06E48420EC54260055D09A /* GRMustache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC4A7371F8A1404008FF15F /* GRMustache.framework */; }; 4C06E48720EC54260055D09A /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C24DC751A93C9CE0098D1BE /* Sparkle.framework */; }; 4C06E48820EC542C0055D09A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CCE521A18C7BE0600D49601 /* AudioToolbox.framework */; }; 4C06E48920EC542C0055D09A /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C46CCBC1580469E00846B64 /* Cocoa.framework */; }; 4C06E48A20EC542C0055D09A /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CA8EC621B63C2970087BF72 /* CoreServices.framework */; }; 4C06E48C20EC542C0055D09A /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C10C00D1F37E59B0004C624 /* IOKit.framework */; }; 4C06E48E20EC542C0055D09A /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF4D6B619837C1700FB5AF0 /* QuartzCore.framework */; }; 4C06E48F20EC542C0055D09A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C46CCC01580469E00846B64 /* Security.framework */; }; 4C06E49220EC542C0055D09A /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C46CCC11580469E00846B64 /* SystemConfiguration.framework */; }; 4C06E49320EC542C0055D09A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C46CCC21580469E00846B64 /* WebKit.framework */; }; 4C06E57F20EC553A0055D09A /* TXAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529E20EB673E00448776 /* TXAppearance.m */; }; 4C06E58020EC553A0055D09A /* TXApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529C20EB673E00448776 /* TXApplication.m */; }; 4C06E58120EC553A0055D09A /* TXGlobalModels.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529B20EB673E00448776 /* TXGlobalModels.m */; }; 4C06E58220EC553A0055D09A /* TXMasterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529F20EB673E00448776 /* TXMasterController.m */; }; 4C06E58320EC553A0055D09A /* TXMenuController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A120EB673E00448776 /* TXMenuController.m */; }; 4C06E58420EC553A0055D09A /* TXSharedApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529D20EB673E00448776 /* TXSharedApplication.m */; }; 4C06E58520EC553A0055D09A /* TXWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A020EB673E00448776 /* TXWindowController.m */; }; 4C06E58A20EC553A0055D09A /* TDCChannelSpotlightControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526B20EB673E00448776 /* TDCChannelSpotlightControls.m */; }; 4C06E58B20EC553A0055D09A /* TDCChannelSpotlightSearchResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526C20EB673E00448776 /* TDCChannelSpotlightSearchResult.m */; }; 4C06E58C20EC553A0055D09A /* TDCChannelSpotlightController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526D20EB673E00448776 /* TDCChannelSpotlightController.m */; }; 4C06E58D20EC553A0055D09A /* TDCChannelSpotlightAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526E20EB673E00448776 /* TDCChannelSpotlightAppearance.m */; }; 4C06E58E20EC553A0055D09A /* TDCChannelSpotlightSearchResultsTable.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526F20EB673E00448776 /* TDCChannelSpotlightSearchResultsTable.m */; }; 4C06E58F20EC553A0055D09A /* TDCFileTransferDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529120EB673E00448776 /* TDCFileTransferDialog.m */; }; 4C06E59020EC553A0055D09A /* TDCFileTransferDialogTransferController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529220EB673E00448776 /* TDCFileTransferDialogTransferController.m */; }; 4C06E59120EC553A0055D09A /* TDCFileTransferDialogTableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529320EB673E00448776 /* TDCFileTransferDialogTableCell.m */; }; 4C06E59220EC553A0055D09A /* TDCLicenseUpgradeDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527420EB673E00448776 /* TDCLicenseUpgradeDialog.m */; }; 4C06E59320EC553A0055D09A /* TDCLicenseUpgradeEligibilitySheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527520EB673E00448776 /* TDCLicenseUpgradeEligibilitySheet.m */; }; 4C06E59420EC553A0055D09A /* TDCLicenseManagerMigrateAppStoreSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527620EB673E00448776 /* TDCLicenseManagerMigrateAppStoreSheet.m */; }; 4C06E59520EC553A0055D09A /* TDCLicenseUpgradeCommonActions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527720EB673E00448776 /* TDCLicenseUpgradeCommonActions.m */; }; 4C06E59620EC553A0055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527820EB673E00448776 /* TDCLicenseManagerRecoverLostLicenseSheet.m */; }; 4C06E59720EC553A0055D09A /* TDCLicenseUpgradeActivateSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527920EB673E00448776 /* TDCLicenseUpgradeActivateSheet.m */; }; 4C06E59820EC553A0055D09A /* TDCLicenseManagerDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527A20EB673E00448776 /* TDCLicenseManagerDialog.m */; }; 4C06E59D20EC553A0055D09A /* TDCPreferencesNotificationConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526820EB673E00448776 /* TDCPreferencesNotificationConfiguration.m */; }; 4C06E59E20EC553A0055D09A /* TDCPreferencesController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526920EB673E00448776 /* TDCPreferencesController.m */; }; 4C06E59F20EC553A0055D09A /* TDCServerEndpointListSheetTable.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528E20EB673E00448776 /* TDCServerEndpointListSheetTable.m */; }; 4C06E5A020EC553A0055D09A /* TDCServerEndpointListSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528F20EB673E00448776 /* TDCServerEndpointListSheet.m */; }; 4C06E5A120EC553A0055D09A /* TDCAboutDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528120EB673E00448776 /* TDCAboutDialog.m */; }; 4C06E5A220EC553A0055D09A /* TDCAddressBookSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527020EB673E00448776 /* TDCAddressBookSheet.m */; }; 4C06E5A320EC553A0055D09A /* TDCAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528020EB673E00448776 /* TDCAlert.m */; }; 4C06E5A420EC553A0055D09A /* TDCChannelBanListSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528220EB673E00448776 /* TDCChannelBanListSheet.m */; }; 4C06E5A520EC553A0055D09A /* TDCChannelInviteSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526320EB673E00448776 /* TDCChannelInviteSheet.m */; }; 4C06E5A620EC553A0055D09A /* TDCChannelModifyModesSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526620EB673E00448776 /* TDCChannelModifyModesSheet.m */; }; 4C06E5A720EC553A0055D09A /* TDCChannelModifyTopicSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528920EB673E00448776 /* TDCChannelModifyTopicSheet.m */; }; 4C06E5A820EC553A0055D09A /* TDCChannelPropertiesNotificationConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527120EB673E00448776 /* TDCChannelPropertiesNotificationConfiguration.m */; }; 4C06E5A920EC553A0055D09A /* TDCChannelPropertiesSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526420EB673E00448776 /* TDCChannelPropertiesSheet.m */; }; 4C06E5AA20EC553A0055D09A /* TDCHighlightEntrySheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528320EB673E00448776 /* TDCHighlightEntrySheet.m */; }; 4C06E5AB20EC553A0055D09A /* TDCInputPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528520EB673E00448776 /* TDCInputPrompt.m */; }; 4C06E5AC20EC553A0055D09A /* TDCNicknameColorSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529420EB673E00448776 /* TDCNicknameColorSheet.m */; }; 4C06E5AD20EC553A0055D09A /* TDCProgressIndicatorSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528820EB673E00448776 /* TDCProgressIndicatorSheet.m */; }; 4C06E5AE20EC553A0055D09A /* TDCServerChangeNicknameSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528C20EB673E00448776 /* TDCServerChangeNicknameSheet.m */; }; 4C06E5AF20EC553A0055D09A /* TDCServerChannelListDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528620EB673E00448776 /* TDCServerChannelListDialog.m */; }; 4C06E5B020EC553A0055D09A /* TDCServerHighlightListSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528720EB673E00448776 /* TDCServerHighlightListSheet.m */; }; 4C06E5B120EC553A0055D09A /* TDCServerPropertiesSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526520EB673E00448776 /* TDCServerPropertiesSheet.m */; }; 4C06E5B220EC553A0055D09A /* TDCSheetBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528A20EB673E00448776 /* TDCSheetBase.m */; }; 4C06E5B320EC553A0055D09A /* TDCWelcomeSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528B20EB673E00448776 /* TDCWelcomeSheet.m */; }; 4C06E5B420EC553A0055D09A /* TDCWindowBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528420EB673E00448776 /* TDCWindowBase.m */; }; 4C06E5B520EC553A0055D09A /* NSColorHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E520EB673E00448776 /* NSColorHelper.m */; }; 4C06E5B620EC553A0055D09A /* NSObjectHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E2A120EC4FA50055D09A /* NSObjectHelper.m */; }; 4C06E5B720EC553A0055D09A /* NSStringHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E720EB673E00448776 /* NSStringHelper.m */; }; 4C06E5B820EC553A0055D09A /* NSTableViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E820EB673E00448776 /* NSTableViewHelper.m */; }; 4C06E5B920EC553A0055D09A /* NSViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E620EB673E00448776 /* NSViewHelper.m */; }; 4C06E5BA20EC553A0055D09A /* TXAppearanceHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E920EB673E00448776 /* TXAppearanceHelper.m */; }; 4C06E5BB20EC553A0055D09A /* WebScriptObjectHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152DC20EB673E00448776 /* WebScriptObjectHelper.m */; }; 4C06E5BC20EC553A0055D09A /* GTMEncodeHTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152DE20EB673E00448776 /* GTMEncodeHTML.m */; }; 4C06E5BD20EC553A0055D09A /* THOPluginDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E120EB673E00448776 /* THOPluginDispatcher.m */; }; 4C06E5BE20EC553A0055D09A /* THOPluginManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E220EB673E00448776 /* THOPluginManager.m */; }; 4C06E5BF20EC553A0055D09A /* THOPluginItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E320EB673E00448776 /* THOPluginItem.m */; }; 4C06E5C020EC553A0055D09A /* THOUnicodeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152DF20EB673E00448776 /* THOUnicodeHelper.m */; }; 4C06E5C120EC553A0055D09A /* IRCAddressBook.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512320EB673E00448776 /* IRCAddressBook.m */; }; 4C06E5C220EC553A0055D09A /* IRCAddressBookMatchCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512B20EB673E00448776 /* IRCAddressBookMatchCache.m */; }; 4C06E5C320EC553A0055D09A /* IRCAddressBookUserTracking.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513E20EB673E00448776 /* IRCAddressBookUserTracking.m */; }; 4C06E5C420EC553A0055D09A /* IRCChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512820EB673E00448776 /* IRCChannel.m */; }; 4C06E5C520EC553A0055D09A /* IRCChannelConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512420EB673E00448776 /* IRCChannelConfig.m */; }; 4C06E5C620EC553A0055D09A /* IRCChannelMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514020EB673E00448776 /* IRCChannelMode.m */; }; 4C06E5C720EC553A0055D09A /* IRCChannelUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513D20EB673E00448776 /* IRCChannelUser.m */; }; 4C06E5C820EC553A0055D09A /* IRCClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513B20EB673E00448776 /* IRCClient.m */; }; 4C06E5C920EC553A0055D09A /* IRCClientConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513C20EB673E00448776 /* IRCClientConfig.m */; }; 4C06E5CA20EC553A0055D09A /* IRCClientRequestedCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513820EB673E00448776 /* IRCClientRequestedCommands.m */; }; 4C06E5CB20EC553A0055D09A /* IRCCommandIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513720EB673E00448776 /* IRCCommandIndex.m */; }; 4C06E5CC20EC553A0055D09A /* IRCConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512E20EB673E00448776 /* IRCConnection.m */; }; 4C06E5CD20EC553A0055D09A /* IRCConnectionConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E29C20EC4F880055D09A /* IRCConnectionConfig.m */; }; 4C06E5CE20EC553A0055D09A /* IRCExtras.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513120EB673E00448776 /* IRCExtras.m */; }; 4C06E5CF20EC553A0055D09A /* IRCHighlightLogEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512620EB673E00448776 /* IRCHighlightLogEntry.m */; }; 4C06E5D020EC553A0055D09A /* IRCHighlightMatchCondition.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512A20EB673E00448776 /* IRCHighlightMatchCondition.m */; }; 4C06E5D120EC553A0055D09A /* IRCISupportInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514120EB673E00448776 /* IRCISupportInfo.m */; }; 4C06E5D220EC553A0055D09A /* IRCMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512D20EB673E00448776 /* IRCMessage.m */; }; 4C06E5D320EC553A0055D09A /* IRCMessageBatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513F20EB673E00448776 /* IRCMessageBatch.m */; }; 4C06E5D420EC553A0055D09A /* IRCModeInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513020EB673E00448776 /* IRCModeInfo.m */; }; 4C06E5D520EC553A0055D09A /* IRCNetworkList.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513920EB673E00448776 /* IRCNetworkList.m */; }; 4C06E5D620EC553A0055D09A /* IRCPrefix.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512520EB673E00448776 /* IRCPrefix.m */; }; 4C06E5D720EC553A0055D09A /* IRCSendingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512C20EB673E00448776 /* IRCSendingMessage.m */; }; 4C06E5D820EC553A0055D09A /* IRCServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512920EB673E00448776 /* IRCServer.m */; }; 4C06E5D920EC553A0055D09A /* IRCTimerCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512F20EB673E00448776 /* IRCTimerCommand.m */; }; 4C06E5DA20EC553A0055D09A /* IRCTreeItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514220EB673E00448776 /* IRCTreeItem.m */; }; 4C06E5DB20EC553A0055D09A /* IRCWorld.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513A20EB673E00448776 /* IRCWorld.m */; }; 4C06E5DD20EC553A0055D09A /* IRCUserRelations.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513320EB673E00448776 /* IRCUserRelations.m */; }; 4C06E5DE20EC553A0055D09A /* IRCUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513420EB673E00448776 /* IRCUser.m */; }; 4C06E5DF20EC553A0055D09A /* IRCUserNicknameColorStyleGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513520EB673E00448776 /* IRCUserNicknameColorStyleGenerator.m */; }; 4C06E5E020EC553A0055D09A /* IRCUserPersistentStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513620EB673E00448776 /* IRCUserPersistentStore.m */; }; 4C06E5E120EC553A0055D09A /* IRCColorFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515F20EB673E00448776 /* IRCColorFormat.m */; }; 4C06E5E220EC553A0055D09A /* OELReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514820EB673E00448776 /* OELReachability.m */; }; 4C06E5E320EC553A0055D09A /* TLOLicenseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514E20EB673E00448776 /* TLOLicenseManager.m */; }; 4C06E5E420EC553A0055D09A /* TLOLicenseManagerDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515020EB673E00448776 /* TLOLicenseManagerDownloader.m */; }; 4C06E5E520EC553A0055D09A /* TLOLicenseManagerLastGen.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514F20EB673E00448776 /* TLOLicenseManagerLastGen.m */; }; 4C06E5E720EC553A0055D09A /* TLOEncryptionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515320EB673E00448776 /* TLOEncryptionManager.m */; }; 4C06E5E820EC553A0055D09A /* TLOFileLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515B20EB673E00448776 /* TLOFileLogger.m */; }; 4C06E5E920EC553A0055D09A /* TLONotificationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514920EB673E00448776 /* TLONotificationController.m */; }; 4C06E5EA20EC553A0055D09A /* TLOInputHistory.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515420EB673E00448776 /* TLOInputHistory.m */; }; 4C06E5EB20EC553A0055D09A /* TLOInternetAddressLookup.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515620EB673E00448776 /* TLOInternetAddressLookup.m */; }; 4C06E5EC20EC553A0055D09A /* TLOKeyEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514B20EB673E00448776 /* TLOKeyEventHandler.m */; }; 4C06E5ED20EC553A0055D09A /* TLOLocalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E29220EC4F570055D09A /* TLOLocalization.m */; }; 4C06E5EE20EC553A0055D09A /* TLONicknameCompletionStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515C20EB673E00448776 /* TLONicknameCompletionStatus.m */; }; 4C06E5EF20EC553A0055D09A /* TLONotificationConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515D20EB673E00448776 /* TLONotificationConfiguration.m */; }; 4C06E5F120EC553A0055D09A /* TLOSoundPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515820EB673E00448776 /* TLOSoundPlayer.m */; }; 4C06E5F220EC553A0055D09A /* TLOSpeechSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515720EB673E00448776 /* TLOSpeechSynthesizer.m */; }; 4C06E5F320EC553A0055D09A /* TLOSpokenNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515A20EB673E00448776 /* TLOSpokenNotification.m */; }; 4C06E5F420EC553A0055D09A /* TLOTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E29320EC4F570055D09A /* TLOTimer.m */; }; 4C06E5F520EC553A0055D09A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514420EB673E00448776 /* main.m */; }; 4C06E5F820EC553A0055D09A /* TPCThemeController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511F20EB673E00448776 /* TPCThemeController.m */; }; 4C06E5F920EC553A0055D09A /* TPCTheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512020EB673E00448776 /* TPCTheme.m */; }; 4C06E5FA20EC553A0055D09A /* TPCApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511420EB673E00448776 /* TPCApplicationInfo.m */; }; 4C06E5FB20EC553A0055D09A /* TPCPathInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511A20EB673E00448776 /* TPCPathInfo.m */; }; 4C06E5FC20EC553A0055D09A /* TPCPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E28820EC4F000055D09A /* TPCPreferences.m */; }; 4C06E5FD20EC553A0055D09A /* TPCPreferencesLocal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511520EB673E00448776 /* TPCPreferencesLocal.m */; }; 4C06E5FE20EC553A0055D09A /* TPCPreferencesImportExport.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511620EB673E00448776 /* TPCPreferencesImportExport.m */; }; 4C06E5FF20EC553A0055D09A /* TPCPreferencesReload.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512120EB673E00448776 /* TPCPreferencesReload.m */; }; 4C06E60020EC553A0055D09A /* TPCPreferencesUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E28920EC4F000055D09A /* TPCPreferencesUserDefaults.m */; }; 4C06E60120EC553A0055D09A /* TPCPreferencesUserDefaultsLocal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511920EB673E00448776 /* TPCPreferencesUserDefaultsLocal.m */; }; 4C06E60320EC553A0055D09A /* TPCResourceManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511820EB673E00448776 /* TPCResourceManager.m */; }; 4C06E60420EC553A0055D09A /* ICLPayloadLocal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E34E20EC50D40055D09A /* ICLPayloadLocal.m */; }; 4C06E60620EC553A0055D09A /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E36320EC51370055D09A /* GCDAsyncSocket.m */; settings = {COMPILER_FLAGS = "-Wno-implicit-retain-self"; }; }; 4C06E60720EC553A0055D09A /* GCDAsyncSocketExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E36520EC51370055D09A /* GCDAsyncSocketExtensions.m */; }; 4C06E60920EC553A0055D09A /* ICLPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E38B20EC51370055D09A /* ICLPayload.m */; }; 4C06E60A20EC553A0055D09A /* TVCLogLineXPC.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E38620EC51370055D09A /* TVCLogLineXPC.m */; }; 4C06E60B20EC553A0055D09A /* TVCChannelSelectionOutlineCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152AE20EB673E00448776 /* TVCChannelSelectionOutlineCellView.m */; }; 4C06E60C20EC553A0055D09A /* TVCChannelSelectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152AF20EB673E00448776 /* TVCChannelSelectionViewController.m */; }; 4C06E60D20EC553A0055D09A /* TVCLogControllerHistoricLogFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C220EB673E00448776 /* TVCLogControllerHistoricLogFile.m */; }; 4C06E60E20EC553A0055D09A /* TVCLogControllerInlineMediaService.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C120EB673E00448776 /* TVCLogControllerInlineMediaService.m */; }; 4C06E60F20EC553A0055D09A /* TVCLogControllerOperationQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C320EB673E00448776 /* TVCLogControllerOperationQueue.m */; }; 4C06E61020EC553A0055D09A /* TVCLogController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BA20EB673E00448776 /* TVCLogController.m */; }; 4C06E61120EC553A0055D09A /* TVCLogLine.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BE20EB673E00448776 /* TVCLogLine.m */; }; 4C06E61220EC553A0055D09A /* TVCLogPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B820EB673E00448776 /* TVCLogPolicy.m */; }; 4C06E61320EC553A0055D09A /* TVCLogRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BB20EB673E00448776 /* TVCLogRenderer.m */; }; 4C06E61420EC553A0055D09A /* TVCLogScriptEventSink.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B720EB673E00448776 /* TVCLogScriptEventSink.m */; }; 4C06E61520EC553A0055D09A /* TVCLogView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BD20EB673E00448776 /* TVCLogView.m */; }; 4C06E61620EC553A0055D09A /* TVCLogViewInternalWK1.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BC20EB673E00448776 /* TVCLogViewInternalWK1.m */; }; 4C06E61720EC553A0055D09A /* TVCLogViewInternalWK2.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B920EB673E00448776 /* TVCLogViewInternalWK2.m */; }; 4C06E61820EC553A0055D09A /* TVCWK1AutoScroller.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BF20EB673E00448776 /* TVCWK1AutoScroller.m */; }; 4C06E61920EC553A0055D09A /* TVCAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C920EB673E00448776 /* TVCAlert.m */; }; 4C06E61A20EC553A0055D09A /* TVCErrorMessagePopover.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C720EB673E00448776 /* TVCErrorMessagePopover.m */; }; 4C06E61B20EC553A0055D09A /* TVCErrorMessagePopoverController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C820EB673E00448776 /* TVCErrorMessagePopoverController.m */; }; 4C06E61C20EC553A0055D09A /* TVCAutoExpandingTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152AA20EB673E00448776 /* TVCAutoExpandingTextField.m */; }; 4C06E61D20EC553A0055D09A /* TVCAutoExpandingTokenField.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A520EB673E00448776 /* TVCAutoExpandingTokenField.m */; }; 4C06E61E20EC553A0055D09A /* TVCTextFormatterMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A620EB673E00448776 /* TVCTextFormatterMenu.m */; }; 4C06E61F20EC553A0055D09A /* TVCTextViewWithIRCFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A720EB673E00448776 /* TVCTextViewWithIRCFormatter.m */; }; 4C06E62020EC553A0055D09A /* TVCValidatedComboBox.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A820EB673E00448776 /* TVCValidatedComboBox.m */; }; 4C06E62120EC553A0055D09A /* TVCValidatedTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A920EB673E00448776 /* TVCValidatedTextField.m */; }; 4C06E62220EC553A0055D09A /* TVCMainWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D120EB673E00448776 /* TVCMainWindow.m */; }; 4C06E62320EC553A0055D09A /* TVCMainWindowAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D520EB673E00448776 /* TVCMainWindowAppearance.m */; }; 4C06E62420EC553A0055D09A /* TVCMainWindowChannelView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D820EB673E00448776 /* TVCMainWindowChannelView.m */; }; 4C06E62520EC553A0055D09A /* TVCMainWindowLoadingScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D220EB673E00448776 /* TVCMainWindowLoadingScreen.m */; }; 4C06E62620EC553A0055D09A /* TVCMainWindowSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D420EB673E00448776 /* TVCMainWindowSegmentedControl.m */; }; 4C06E62820EC553A0055D09A /* TVCMainWindowSplitView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D320EB673E00448776 /* TVCMainWindowSplitView.m */; }; 4C06E62920EC553A0055D09A /* TVCMainWindowTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D620EB673E00448776 /* TVCMainWindowTextView.m */; }; 4C06E62A20EC553A0055D09A /* TVCMainWindowTextViewAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152CF20EB673E00448776 /* TVCMainWindowTextViewAppearance.m */; }; 4C06E62B20EC553A0055D09A /* TVCMainWindowTitlebarAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D720EB673E00448776 /* TVCMainWindowTitlebarAccessoryView.m */; }; 4C06E62C20EC553A0055D09A /* TVCNotificationConfigurationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152AC20EB673E00448776 /* TVCNotificationConfigurationViewController.m */; }; 4C06E62D20EC553A0055D09A /* TVCServerList.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152CC20EB673E00448776 /* TVCServerList.m */; }; 4C06E62E20EC553A0055D09A /* TVCServerListAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152CD20EB673E00448776 /* TVCServerListAppearance.m */; }; 4C06E62F20EC553A0055D09A /* TVCServerListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152CB20EB673E00448776 /* TVCServerListCell.m */; }; 4C06E63020EC553A0055D09A /* TVCMemberList.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B320EB673E00448776 /* TVCMemberList.m */; }; 4C06E63120EC553A0055D09A /* TVCMemberListAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B220EB673E00448776 /* TVCMemberListAppearance.m */; }; 4C06E63220EC553A0055D09A /* TVCMemberListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B120EB673E00448776 /* TVCMemberListCell.m */; }; 4C06E63320EC553A0055D09A /* TVCMemberListUserInfoPopover.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B420EB673E00448776 /* TVCMemberListUserInfoPopover.m */; }; 4C06E63420EC553A0055D09A /* TVCAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C420EB673E00448776 /* TVCAppearance.m */; }; 4C06E63520EC553A0055D09A /* TVCBasicTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A320EB673E00448776 /* TVCBasicTableView.m */; }; 4C06E63620EC553A0055D09A /* TVCContentNavigationOutlineView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B520EB673E00448776 /* TVCContentNavigationOutlineView.m */; }; 4C06E63720EC553A0055D09A /* TVCDockIcon.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C520EB673E00448776 /* TVCDockIcon.m */; }; 4C06E63820EC55B90055D09A /* TXAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529E20EB673E00448776 /* TXAppearance.m */; }; 4C06E63920EC55B90055D09A /* TXApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529C20EB673E00448776 /* TXApplication.m */; }; 4C06E63A20EC55B90055D09A /* TXGlobalModels.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529B20EB673E00448776 /* TXGlobalModels.m */; }; 4C06E63B20EC55B90055D09A /* TXMasterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529F20EB673E00448776 /* TXMasterController.m */; }; 4C06E63C20EC55B90055D09A /* TXMenuController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A120EB673E00448776 /* TXMenuController.m */; }; 4C06E63D20EC55B90055D09A /* TXSharedApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529D20EB673E00448776 /* TXSharedApplication.m */; }; 4C06E63E20EC55B90055D09A /* TXWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A020EB673E00448776 /* TXWindowController.m */; }; 4C06E64320EC55B90055D09A /* TDCChannelSpotlightControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526B20EB673E00448776 /* TDCChannelSpotlightControls.m */; }; 4C06E64420EC55B90055D09A /* TDCChannelSpotlightSearchResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526C20EB673E00448776 /* TDCChannelSpotlightSearchResult.m */; }; 4C06E64520EC55B90055D09A /* TDCChannelSpotlightController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526D20EB673E00448776 /* TDCChannelSpotlightController.m */; }; 4C06E64620EC55B90055D09A /* TDCChannelSpotlightAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526E20EB673E00448776 /* TDCChannelSpotlightAppearance.m */; }; 4C06E64720EC55B90055D09A /* TDCChannelSpotlightSearchResultsTable.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526F20EB673E00448776 /* TDCChannelSpotlightSearchResultsTable.m */; }; 4C06E64820EC55B90055D09A /* TDCFileTransferDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529120EB673E00448776 /* TDCFileTransferDialog.m */; }; 4C06E64920EC55B90055D09A /* TDCFileTransferDialogTransferController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529220EB673E00448776 /* TDCFileTransferDialogTransferController.m */; }; 4C06E64A20EC55B90055D09A /* TDCFileTransferDialogTableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529320EB673E00448776 /* TDCFileTransferDialogTableCell.m */; }; 4C06E64B20EC55B90055D09A /* TDCLicenseUpgradeDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527420EB673E00448776 /* TDCLicenseUpgradeDialog.m */; }; 4C06E64C20EC55B90055D09A /* TDCLicenseUpgradeEligibilitySheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527520EB673E00448776 /* TDCLicenseUpgradeEligibilitySheet.m */; }; 4C06E64D20EC55B90055D09A /* TDCLicenseManagerMigrateAppStoreSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527620EB673E00448776 /* TDCLicenseManagerMigrateAppStoreSheet.m */; }; 4C06E64E20EC55B90055D09A /* TDCLicenseUpgradeCommonActions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527720EB673E00448776 /* TDCLicenseUpgradeCommonActions.m */; }; 4C06E64F20EC55B90055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527820EB673E00448776 /* TDCLicenseManagerRecoverLostLicenseSheet.m */; }; 4C06E65020EC55B90055D09A /* TDCLicenseUpgradeActivateSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527920EB673E00448776 /* TDCLicenseUpgradeActivateSheet.m */; }; 4C06E65120EC55B90055D09A /* TDCLicenseManagerDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527A20EB673E00448776 /* TDCLicenseManagerDialog.m */; }; 4C06E65620EC55B90055D09A /* TDCPreferencesNotificationConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526820EB673E00448776 /* TDCPreferencesNotificationConfiguration.m */; }; 4C06E65720EC55B90055D09A /* TDCPreferencesController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526920EB673E00448776 /* TDCPreferencesController.m */; }; 4C06E65820EC55B90055D09A /* TDCServerEndpointListSheetTable.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528E20EB673E00448776 /* TDCServerEndpointListSheetTable.m */; }; 4C06E65920EC55B90055D09A /* TDCServerEndpointListSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528F20EB673E00448776 /* TDCServerEndpointListSheet.m */; }; 4C06E65A20EC55B90055D09A /* TDCAboutDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528120EB673E00448776 /* TDCAboutDialog.m */; }; 4C06E65B20EC55B90055D09A /* TDCAddressBookSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527020EB673E00448776 /* TDCAddressBookSheet.m */; }; 4C06E65C20EC55B90055D09A /* TDCAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528020EB673E00448776 /* TDCAlert.m */; }; 4C06E65D20EC55B90055D09A /* TDCChannelBanListSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528220EB673E00448776 /* TDCChannelBanListSheet.m */; }; 4C06E65E20EC55B90055D09A /* TDCChannelInviteSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526320EB673E00448776 /* TDCChannelInviteSheet.m */; }; 4C06E65F20EC55B90055D09A /* TDCChannelModifyModesSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526620EB673E00448776 /* TDCChannelModifyModesSheet.m */; }; 4C06E66020EC55B90055D09A /* TDCChannelModifyTopicSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528920EB673E00448776 /* TDCChannelModifyTopicSheet.m */; }; 4C06E66120EC55B90055D09A /* TDCChannelPropertiesNotificationConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31527120EB673E00448776 /* TDCChannelPropertiesNotificationConfiguration.m */; }; 4C06E66220EC55B90055D09A /* TDCChannelPropertiesSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526420EB673E00448776 /* TDCChannelPropertiesSheet.m */; }; 4C06E66320EC55B90055D09A /* TDCHighlightEntrySheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528320EB673E00448776 /* TDCHighlightEntrySheet.m */; }; 4C06E66420EC55B90055D09A /* TDCInputPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528520EB673E00448776 /* TDCInputPrompt.m */; }; 4C06E66520EC55B90055D09A /* TDCNicknameColorSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31529420EB673E00448776 /* TDCNicknameColorSheet.m */; }; 4C06E66620EC55B90055D09A /* TDCProgressIndicatorSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528820EB673E00448776 /* TDCProgressIndicatorSheet.m */; }; 4C06E66720EC55B90055D09A /* TDCServerChangeNicknameSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528C20EB673E00448776 /* TDCServerChangeNicknameSheet.m */; }; 4C06E66820EC55B90055D09A /* TDCServerChannelListDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528620EB673E00448776 /* TDCServerChannelListDialog.m */; }; 4C06E66920EC55B90055D09A /* TDCServerHighlightListSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528720EB673E00448776 /* TDCServerHighlightListSheet.m */; }; 4C06E66A20EC55B90055D09A /* TDCServerPropertiesSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31526520EB673E00448776 /* TDCServerPropertiesSheet.m */; }; 4C06E66B20EC55B90055D09A /* TDCSheetBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528A20EB673E00448776 /* TDCSheetBase.m */; }; 4C06E66C20EC55B90055D09A /* TDCWelcomeSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528B20EB673E00448776 /* TDCWelcomeSheet.m */; }; 4C06E66D20EC55B90055D09A /* TDCWindowBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31528420EB673E00448776 /* TDCWindowBase.m */; }; 4C06E66E20EC55B90055D09A /* NSColorHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E520EB673E00448776 /* NSColorHelper.m */; }; 4C06E66F20EC55B90055D09A /* NSObjectHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E2A120EC4FA50055D09A /* NSObjectHelper.m */; }; 4C06E67020EC55B90055D09A /* NSStringHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E720EB673E00448776 /* NSStringHelper.m */; }; 4C06E67120EC55B90055D09A /* NSTableViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E820EB673E00448776 /* NSTableViewHelper.m */; }; 4C06E67220EC55B90055D09A /* NSViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E620EB673E00448776 /* NSViewHelper.m */; }; 4C06E67320EC55B90055D09A /* TXAppearanceHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E920EB673E00448776 /* TXAppearanceHelper.m */; }; 4C06E67420EC55B90055D09A /* WebScriptObjectHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152DC20EB673E00448776 /* WebScriptObjectHelper.m */; }; 4C06E67520EC55B90055D09A /* GTMEncodeHTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152DE20EB673E00448776 /* GTMEncodeHTML.m */; }; 4C06E67620EC55B90055D09A /* THOPluginDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E120EB673E00448776 /* THOPluginDispatcher.m */; }; 4C06E67720EC55B90055D09A /* THOPluginManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E220EB673E00448776 /* THOPluginManager.m */; }; 4C06E67820EC55B90055D09A /* THOPluginItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152E320EB673E00448776 /* THOPluginItem.m */; }; 4C06E67920EC55B90055D09A /* THOUnicodeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152DF20EB673E00448776 /* THOUnicodeHelper.m */; }; 4C06E67A20EC55B90055D09A /* IRCAddressBook.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512320EB673E00448776 /* IRCAddressBook.m */; }; 4C06E67B20EC55B90055D09A /* IRCAddressBookMatchCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512B20EB673E00448776 /* IRCAddressBookMatchCache.m */; }; 4C06E67C20EC55B90055D09A /* IRCAddressBookUserTracking.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513E20EB673E00448776 /* IRCAddressBookUserTracking.m */; }; 4C06E67D20EC55B90055D09A /* IRCChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512820EB673E00448776 /* IRCChannel.m */; }; 4C06E67E20EC55B90055D09A /* IRCChannelConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512420EB673E00448776 /* IRCChannelConfig.m */; }; 4C06E67F20EC55B90055D09A /* IRCChannelMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514020EB673E00448776 /* IRCChannelMode.m */; }; 4C06E68020EC55B90055D09A /* IRCChannelUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513D20EB673E00448776 /* IRCChannelUser.m */; }; 4C06E68120EC55B90055D09A /* IRCClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513B20EB673E00448776 /* IRCClient.m */; }; 4C06E68220EC55B90055D09A /* IRCClientConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513C20EB673E00448776 /* IRCClientConfig.m */; }; 4C06E68320EC55B90055D09A /* IRCClientRequestedCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513820EB673E00448776 /* IRCClientRequestedCommands.m */; }; 4C06E68420EC55B90055D09A /* IRCCommandIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513720EB673E00448776 /* IRCCommandIndex.m */; }; 4C06E68520EC55B90055D09A /* IRCConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512E20EB673E00448776 /* IRCConnection.m */; }; 4C06E68620EC55B90055D09A /* IRCConnectionConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E29C20EC4F880055D09A /* IRCConnectionConfig.m */; }; 4C06E68720EC55B90055D09A /* IRCExtras.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513120EB673E00448776 /* IRCExtras.m */; }; 4C06E68820EC55B90055D09A /* IRCHighlightLogEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512620EB673E00448776 /* IRCHighlightLogEntry.m */; }; 4C06E68920EC55B90055D09A /* IRCHighlightMatchCondition.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512A20EB673E00448776 /* IRCHighlightMatchCondition.m */; }; 4C06E68A20EC55B90055D09A /* IRCISupportInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514120EB673E00448776 /* IRCISupportInfo.m */; }; 4C06E68B20EC55B90055D09A /* IRCMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512D20EB673E00448776 /* IRCMessage.m */; }; 4C06E68C20EC55B90055D09A /* IRCMessageBatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513F20EB673E00448776 /* IRCMessageBatch.m */; }; 4C06E68D20EC55B90055D09A /* IRCModeInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513020EB673E00448776 /* IRCModeInfo.m */; }; 4C06E68E20EC55B90055D09A /* IRCNetworkList.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513920EB673E00448776 /* IRCNetworkList.m */; }; 4C06E68F20EC55B90055D09A /* IRCPrefix.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512520EB673E00448776 /* IRCPrefix.m */; }; 4C06E69020EC55B90055D09A /* IRCSendingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512C20EB673E00448776 /* IRCSendingMessage.m */; }; 4C06E69120EC55B90055D09A /* IRCServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512920EB673E00448776 /* IRCServer.m */; }; 4C06E69220EC55B90055D09A /* IRCTimerCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512F20EB673E00448776 /* IRCTimerCommand.m */; }; 4C06E69320EC55B90055D09A /* IRCTreeItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514220EB673E00448776 /* IRCTreeItem.m */; }; 4C06E69420EC55B90055D09A /* IRCWorld.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513A20EB673E00448776 /* IRCWorld.m */; }; 4C06E69620EC55B90055D09A /* IRCUserRelations.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513320EB673E00448776 /* IRCUserRelations.m */; }; 4C06E69720EC55B90055D09A /* IRCUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513420EB673E00448776 /* IRCUser.m */; }; 4C06E69820EC55B90055D09A /* IRCUserNicknameColorStyleGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513520EB673E00448776 /* IRCUserNicknameColorStyleGenerator.m */; }; 4C06E69920EC55B90055D09A /* IRCUserPersistentStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31513620EB673E00448776 /* IRCUserPersistentStore.m */; }; 4C06E69A20EC55B90055D09A /* IRCColorFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515F20EB673E00448776 /* IRCColorFormat.m */; }; 4C06E69B20EC55B90055D09A /* OELReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514820EB673E00448776 /* OELReachability.m */; }; 4C06E69C20EC55B90055D09A /* TLOLicenseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514E20EB673E00448776 /* TLOLicenseManager.m */; }; 4C06E69D20EC55B90055D09A /* TLOLicenseManagerDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515020EB673E00448776 /* TLOLicenseManagerDownloader.m */; }; 4C06E69E20EC55B90055D09A /* TLOLicenseManagerLastGen.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514F20EB673E00448776 /* TLOLicenseManagerLastGen.m */; }; 4C06E6A020EC55B90055D09A /* TLOEncryptionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515320EB673E00448776 /* TLOEncryptionManager.m */; }; 4C06E6A120EC55B90055D09A /* TLOFileLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515B20EB673E00448776 /* TLOFileLogger.m */; }; 4C06E6A220EC55B90055D09A /* TLONotificationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514920EB673E00448776 /* TLONotificationController.m */; }; 4C06E6A320EC55B90055D09A /* TLOInputHistory.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515420EB673E00448776 /* TLOInputHistory.m */; }; 4C06E6A420EC55B90055D09A /* TLOInternetAddressLookup.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515620EB673E00448776 /* TLOInternetAddressLookup.m */; }; 4C06E6A520EC55B90055D09A /* TLOKeyEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514B20EB673E00448776 /* TLOKeyEventHandler.m */; }; 4C06E6A620EC55B90055D09A /* TLOLocalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E29220EC4F570055D09A /* TLOLocalization.m */; }; 4C06E6A720EC55B90055D09A /* TLONicknameCompletionStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515C20EB673E00448776 /* TLONicknameCompletionStatus.m */; }; 4C06E6A820EC55B90055D09A /* TLONotificationConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515D20EB673E00448776 /* TLONotificationConfiguration.m */; }; 4C06E6AA20EC55B90055D09A /* TLOSoundPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515820EB673E00448776 /* TLOSoundPlayer.m */; }; 4C06E6AB20EC55B90055D09A /* TLOSpeechSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515720EB673E00448776 /* TLOSpeechSynthesizer.m */; }; 4C06E6AC20EC55B90055D09A /* TLOSpokenNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515A20EB673E00448776 /* TLOSpokenNotification.m */; }; 4C06E6AD20EC55B90055D09A /* TLOTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E29320EC4F570055D09A /* TLOTimer.m */; }; 4C06E6AE20EC55B90055D09A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31514420EB673E00448776 /* main.m */; }; 4C06E6B120EC55B90055D09A /* TPCThemeController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511F20EB673E00448776 /* TPCThemeController.m */; }; 4C06E6B220EC55B90055D09A /* TPCTheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512020EB673E00448776 /* TPCTheme.m */; }; 4C06E6B320EC55B90055D09A /* TPCApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511420EB673E00448776 /* TPCApplicationInfo.m */; }; 4C06E6B420EC55B90055D09A /* TPCPathInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511A20EB673E00448776 /* TPCPathInfo.m */; }; 4C06E6B520EC55B90055D09A /* TPCPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E28820EC4F000055D09A /* TPCPreferences.m */; }; 4C06E6B620EC55B90055D09A /* TPCPreferencesLocal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511520EB673E00448776 /* TPCPreferencesLocal.m */; }; 4C06E6B720EC55B90055D09A /* TPCPreferencesImportExport.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511620EB673E00448776 /* TPCPreferencesImportExport.m */; }; 4C06E6B820EC55B90055D09A /* TPCPreferencesReload.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31512120EB673E00448776 /* TPCPreferencesReload.m */; }; 4C06E6B920EC55B90055D09A /* TPCPreferencesUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E28920EC4F000055D09A /* TPCPreferencesUserDefaults.m */; }; 4C06E6BA20EC55B90055D09A /* TPCPreferencesUserDefaultsLocal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511920EB673E00448776 /* TPCPreferencesUserDefaultsLocal.m */; }; 4C06E6BC20EC55B90055D09A /* TPCResourceManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C31511820EB673E00448776 /* TPCResourceManager.m */; }; 4C06E6BD20EC55B90055D09A /* ICLPayloadLocal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E34E20EC50D40055D09A /* ICLPayloadLocal.m */; }; 4C06E6BF20EC55B90055D09A /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E36320EC51370055D09A /* GCDAsyncSocket.m */; settings = {COMPILER_FLAGS = "-Wno-implicit-retain-self"; }; }; 4C06E6C020EC55B90055D09A /* GCDAsyncSocketExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E36520EC51370055D09A /* GCDAsyncSocketExtensions.m */; }; 4C06E6C220EC55B90055D09A /* ICLPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E38B20EC51370055D09A /* ICLPayload.m */; }; 4C06E6C320EC55B90055D09A /* TVCLogLineXPC.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C06E38620EC51370055D09A /* TVCLogLineXPC.m */; }; 4C06E6C420EC55B90055D09A /* TVCChannelSelectionOutlineCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152AE20EB673E00448776 /* TVCChannelSelectionOutlineCellView.m */; }; 4C06E6C520EC55B90055D09A /* TVCChannelSelectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152AF20EB673E00448776 /* TVCChannelSelectionViewController.m */; }; 4C06E6C620EC55B90055D09A /* TVCLogControllerHistoricLogFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C220EB673E00448776 /* TVCLogControllerHistoricLogFile.m */; }; 4C06E6C720EC55B90055D09A /* TVCLogControllerInlineMediaService.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C120EB673E00448776 /* TVCLogControllerInlineMediaService.m */; }; 4C06E6C820EC55B90055D09A /* TVCLogControllerOperationQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C320EB673E00448776 /* TVCLogControllerOperationQueue.m */; }; 4C06E6C920EC55B90055D09A /* TVCLogController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BA20EB673E00448776 /* TVCLogController.m */; }; 4C06E6CA20EC55B90055D09A /* TVCLogLine.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BE20EB673E00448776 /* TVCLogLine.m */; }; 4C06E6CB20EC55B90055D09A /* TVCLogPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B820EB673E00448776 /* TVCLogPolicy.m */; }; 4C06E6CC20EC55B90055D09A /* TVCLogRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BB20EB673E00448776 /* TVCLogRenderer.m */; }; 4C06E6CD20EC55B90055D09A /* TVCLogScriptEventSink.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B720EB673E00448776 /* TVCLogScriptEventSink.m */; }; 4C06E6CE20EC55B90055D09A /* TVCLogView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BD20EB673E00448776 /* TVCLogView.m */; }; 4C06E6CF20EC55B90055D09A /* TVCLogViewInternalWK1.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BC20EB673E00448776 /* TVCLogViewInternalWK1.m */; }; 4C06E6D020EC55B90055D09A /* TVCLogViewInternalWK2.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B920EB673E00448776 /* TVCLogViewInternalWK2.m */; }; 4C06E6D120EC55B90055D09A /* TVCWK1AutoScroller.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152BF20EB673E00448776 /* TVCWK1AutoScroller.m */; }; 4C06E6D220EC55B90055D09A /* TVCAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C920EB673E00448776 /* TVCAlert.m */; }; 4C06E6D320EC55B90055D09A /* TVCErrorMessagePopover.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C720EB673E00448776 /* TVCErrorMessagePopover.m */; }; 4C06E6D420EC55B90055D09A /* TVCErrorMessagePopoverController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C820EB673E00448776 /* TVCErrorMessagePopoverController.m */; }; 4C06E6D520EC55B90055D09A /* TVCAutoExpandingTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152AA20EB673E00448776 /* TVCAutoExpandingTextField.m */; }; 4C06E6D620EC55B90055D09A /* TVCAutoExpandingTokenField.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A520EB673E00448776 /* TVCAutoExpandingTokenField.m */; }; 4C06E6D720EC55B90055D09A /* TVCTextFormatterMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A620EB673E00448776 /* TVCTextFormatterMenu.m */; }; 4C06E6D820EC55B90055D09A /* TVCTextViewWithIRCFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A720EB673E00448776 /* TVCTextViewWithIRCFormatter.m */; }; 4C06E6D920EC55B90055D09A /* TVCValidatedComboBox.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A820EB673E00448776 /* TVCValidatedComboBox.m */; }; 4C06E6DA20EC55B90055D09A /* TVCValidatedTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A920EB673E00448776 /* TVCValidatedTextField.m */; }; 4C06E6DB20EC55B90055D09A /* TVCMainWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D120EB673E00448776 /* TVCMainWindow.m */; }; 4C06E6DC20EC55B90055D09A /* TVCMainWindowAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D520EB673E00448776 /* TVCMainWindowAppearance.m */; }; 4C06E6DD20EC55B90055D09A /* TVCMainWindowChannelView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D820EB673E00448776 /* TVCMainWindowChannelView.m */; }; 4C06E6DE20EC55B90055D09A /* TVCMainWindowLoadingScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D220EB673E00448776 /* TVCMainWindowLoadingScreen.m */; }; 4C06E6DF20EC55B90055D09A /* TVCMainWindowSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D420EB673E00448776 /* TVCMainWindowSegmentedControl.m */; }; 4C06E6E120EC55B90055D09A /* TVCMainWindowSplitView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D320EB673E00448776 /* TVCMainWindowSplitView.m */; }; 4C06E6E220EC55B90055D09A /* TVCMainWindowTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D620EB673E00448776 /* TVCMainWindowTextView.m */; }; 4C06E6E320EC55B90055D09A /* TVCMainWindowTextViewAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152CF20EB673E00448776 /* TVCMainWindowTextViewAppearance.m */; }; 4C06E6E420EC55B90055D09A /* TVCMainWindowTitlebarAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152D720EB673E00448776 /* TVCMainWindowTitlebarAccessoryView.m */; }; 4C06E6E520EC55B90055D09A /* TVCNotificationConfigurationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152AC20EB673E00448776 /* TVCNotificationConfigurationViewController.m */; }; 4C06E6E620EC55B90055D09A /* TVCServerList.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152CC20EB673E00448776 /* TVCServerList.m */; }; 4C06E6E720EC55B90055D09A /* TVCServerListAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152CD20EB673E00448776 /* TVCServerListAppearance.m */; }; 4C06E6E820EC55B90055D09A /* TVCServerListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152CB20EB673E00448776 /* TVCServerListCell.m */; }; 4C06E6E920EC55B90055D09A /* TVCMemberList.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B320EB673E00448776 /* TVCMemberList.m */; }; 4C06E6EA20EC55B90055D09A /* TVCMemberListAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B220EB673E00448776 /* TVCMemberListAppearance.m */; }; 4C06E6EB20EC55B90055D09A /* TVCMemberListCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B120EB673E00448776 /* TVCMemberListCell.m */; }; 4C06E6EC20EC55B90055D09A /* TVCMemberListUserInfoPopover.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B420EB673E00448776 /* TVCMemberListUserInfoPopover.m */; }; 4C06E6ED20EC55B90055D09A /* TVCAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C420EB673E00448776 /* TVCAppearance.m */; }; 4C06E6EE20EC55B90055D09A /* TVCBasicTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152A320EB673E00448776 /* TVCBasicTableView.m */; }; 4C06E6EF20EC55B90055D09A /* TVCContentNavigationOutlineView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152B520EB673E00448776 /* TVCContentNavigationOutlineView.m */; }; 4C06E6F020EC55B90055D09A /* TVCDockIcon.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C3152C520EB673E00448776 /* TVCDockIcon.m */; }; 4C0A31152C374A5B0081EF1F /* KeysExcludedFromMigrate.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C0A31122C37419D0081EF1F /* KeysExcludedFromMigrate.plist */; }; 4C0A31162C374A610081EF1F /* KeysExcludedFromMigrate.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C0A31122C37419D0081EF1F /* KeysExcludedFromMigrate.plist */; }; 4C17167D254A58BD004BE8AE /* Inline Content Loader.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 4C171669254A589D004BE8AE /* Inline Content Loader.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4C17167E254A58BD004BE8AE /* IRC Connection Host.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 4C171668254A589D004BE8AE /* IRC Connection Host.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4C17167F254A58BD004BE8AE /* Scrollback History Manager.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 4C17166A254A589E004BE8AE /* Scrollback History Manager.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4C171681254A58C2004BE8AE /* Inline Content Loader.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 4C171669254A589D004BE8AE /* Inline Content Loader.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4C171682254A58C2004BE8AE /* IRC Connection Host.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 4C171668254A589D004BE8AE /* IRC Connection Host.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4C171683254A58C2004BE8AE /* Scrollback History Manager.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 4C17166A254A589E004BE8AE /* Scrollback History Manager.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4C17305522B4A94000DC5836 /* TDCPreferencesUserStyleSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C17305222B4A94000DC5836 /* TDCPreferencesUserStyleSheet.xib */; }; 4C17305622B4A94000DC5836 /* TDCPreferencesUserStyleSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C17305222B4A94000DC5836 /* TDCPreferencesUserStyleSheet.xib */; }; 4C2BEB642C3866DF008F8F0D /* KeysExcludedFromContainer.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C2BEB632C3864C1008F8F0D /* KeysExcludedFromContainer.plist */; }; 4C2BEB652C3866E2008F8F0D /* KeysExcludedFromContainer.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C2BEB632C3864C1008F8F0D /* KeysExcludedFromContainer.plist */; }; 4C2E92212C1E446E003BB92D /* Caffeine.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C2E92202C1E4463003BB92D /* Caffeine.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4C2E92232C1E447E003BB92D /* Caffeine.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C2E92202C1E4463003BB92D /* Caffeine.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4C2F42052C3F4218000D962D /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C2F42042C3F4218000D962D /* UserNotifications.framework */; }; 4C2F42062C3F4218000D962D /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C2F42042C3F4218000D962D /* UserNotifications.framework */; }; 4C31554C20EB6CB300448776 /* IRC.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524120EB673E00448776 /* IRC.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31554D20EB6CB300448776 /* IRCAddressBook.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A720EB673E00448776 /* IRCAddressBook.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31554E20EB6CB300448776 /* IRCAddressBookUserTracking.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517620EB673E00448776 /* IRCAddressBookUserTracking.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31554F20EB6CB300448776 /* IRCChannel.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524B20EB673E00448776 /* IRCChannel.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555020EB6CB300448776 /* IRCChannelConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A420EB673E00448776 /* IRCChannelConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555120EB6CB300448776 /* IRCChannelMode.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519A20EB673E00448776 /* IRCChannelMode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555220EB6CB300448776 /* IRCChannelUser.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517920EB673E00448776 /* IRCChannelUser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555320EB6CB300448776 /* IRCClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517C20EB673E00448776 /* IRCClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555420EB6CB300448776 /* IRCClientConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517F20EB673E00448776 /* IRCClientConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555520EB6CB300448776 /* IRCColorFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31526120EB673E00448776 /* IRCColorFormat.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555620EB6CB300448776 /* IRCCommandIndex.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517120EB673E00448776 /* IRCCommandIndex.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555720EB6CB300448776 /* IRCConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31526020EB673E00448776 /* IRCConnection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555820EB6CB300448776 /* IRCHighlightLogEntry.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A120EB673E00448776 /* IRCHighlightLogEntry.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555920EB6CB300448776 /* IRCHighlightMatchCondition.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524720EB673E00448776 /* IRCHighlightMatchCondition.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555A20EB6CB300448776 /* IRCISupportInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519820EB673E00448776 /* IRCISupportInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555B20EB6CB300448776 /* IRCMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525320EB673E00448776 /* IRCMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555C20EB6CB300448776 /* IRCModeInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516C20EB673E00448776 /* IRCModeInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555D20EB6CB300448776 /* IRCNetworkList.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518120EB673E00448776 /* IRCNetworkList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555E20EB6CB300448776 /* IRCNumerics.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516620EB673E00448776 /* IRCNumerics.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31555F20EB6CB300448776 /* IRCPrefix.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A520EB673E00448776 /* IRCPrefix.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556020EB6CB300448776 /* IRCSendingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525520EB673E00448776 /* IRCSendingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556120EB6CB300448776 /* IRCServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524620EB673E00448776 /* IRCServer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556220EB6CB300448776 /* IRCTreeItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519720EB673E00448776 /* IRCTreeItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556320EB6CB300448776 /* IRCUser.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524E20EB673E00448776 /* IRCUser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556420EB6CB300448776 /* IRCUserRelations.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524F20EB673E00448776 /* IRCUserRelations.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556520EB6CB300448776 /* IRCWorld.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518020EB673E00448776 /* IRCWorld.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556620EB6CB300448776 /* NSColorHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519E20EB673E00448776 /* NSColorHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556720EB6CB300448776 /* NSStringHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525820EB673E00448776 /* NSStringHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556820EB6CB300448776 /* NSViewHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524320EB673E00448776 /* NSViewHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556920EB6CB300448776 /* TDCAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524920EB673E00448776 /* TDCAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556A20EB6CB300448776 /* TDCInputPrompt.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516A20EB673E00448776 /* TDCInputPrompt.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556B20EB6CB300448776 /* TDCSheetBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518220EB673E00448776 /* TDCSheetBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556C20EB6CB300448776 /* TDCWindowBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525C20EB673E00448776 /* TDCWindowBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556D20EB6CB300448776 /* Textual.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525F20EB673E00448776 /* Textual.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556E20EB6CB300448776 /* TextualApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517720EB673E00448776 /* TextualApplication.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31556F20EB6CB300448776 /* THOPluginProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517820EB673E00448776 /* THOPluginProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557020EB6CB300448776 /* THOUnicodeHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A620EB673E00448776 /* THOUnicodeHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557120EB6CB300448776 /* TLOEncryptionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524520EB673E00448776 /* TLOEncryptionManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557220EB6CB300448776 /* TLONotificationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151AA20EB673E00448776 /* TLONotificationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557320EB6CB300448776 /* TLOInternetAddressLookup.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525B20EB673E00448776 /* TLOInternetAddressLookup.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557420EB6CB300448776 /* TLOKeyEventHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A820EB673E00448776 /* TLOKeyEventHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557520EB6CB300448776 /* TLOLinkParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519D20EB673E00448776 /* TLOLinkParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557620EB6CB300448776 /* TLOpenLink.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517B20EB673E00448776 /* TLOpenLink.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557820EB6CB300448776 /* TLOSoundPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516420EB673E00448776 /* TLOSoundPlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557920EB6CB300448776 /* TPCApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524020EB673E00448776 /* TPCApplicationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557A20EB6CB300448776 /* TPCPathInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517D20EB673E00448776 /* TPCPathInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557D20EB6CB300448776 /* TPCPreferencesImportExport.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524A20EB673E00448776 /* TPCPreferencesImportExport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557E20EB6CB300448776 /* TPCPreferencesLocal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516820EB673E00448776 /* TPCPreferencesLocal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31557F20EB6CB300448776 /* TPCPreferencesReload.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519C20EB673E00448776 /* TPCPreferencesReload.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558020EB6CB300448776 /* TPCPreferencesUserDefaultsLocal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519F20EB673E00448776 /* TPCPreferencesUserDefaultsLocal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558120EB6CB300448776 /* TPCResourceManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516520EB673E00448776 /* TPCResourceManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558220EB6CB300448776 /* TPCThemeController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517220EB673E00448776 /* TPCThemeController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558320EB6CB300448776 /* TPCTheme.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517020EB673E00448776 /* TPCTheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558420EB6CB300448776 /* TVCAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516F20EB673E00448776 /* TVCAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558520EB6CB300448776 /* TVCAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516E20EB673E00448776 /* TVCAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558620EB6CB300448776 /* TVCAutoExpandingTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517E20EB673E00448776 /* TVCAutoExpandingTextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558720EB6CB300448776 /* TVCAutoExpandingTokenField.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A320EB673E00448776 /* TVCAutoExpandingTokenField.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558820EB6CB300448776 /* TVCBasicTableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A220EB673E00448776 /* TVCBasicTableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558920EB6CB300448776 /* TVCChannelSelectionViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519620EB673E00448776 /* TVCChannelSelectionViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558A20EB6CB300448776 /* TVCErrorMessagePopoverControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517520EB673E00448776 /* TVCErrorMessagePopoverControllerPrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558B20EB6CB300448776 /* TVCErrorMessagePopoverPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A020EB673E00448776 /* TVCErrorMessagePopoverPrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558C20EB6CB300448776 /* TVCLogController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525620EB673E00448776 /* TVCLogController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558D20EB6CB300448776 /* TVCLogLine.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519920EB673E00448776 /* TVCLogLine.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558E20EB6CB300448776 /* TVCLogRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525420EB673E00448776 /* TVCLogRenderer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31558F20EB6CB300448776 /* TVCLogView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524D20EB673E00448776 /* TVCLogView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559020EB6CB300448776 /* TVCMainWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524C20EB673E00448776 /* TVCMainWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559120EB6CB300448776 /* TVCMainWindowAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525A20EB673E00448776 /* TVCMainWindowAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559220EB6CB300448776 /* TVCMainWindowLoadingScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525720EB673E00448776 /* TVCMainWindowLoadingScreen.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559320EB6CB300448776 /* TVCMainWindowSplitView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525220EB673E00448776 /* TVCMainWindowSplitView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559420EB6CB300448776 /* TVCMainWindowTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525920EB673E00448776 /* TVCMainWindowTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559520EB6CB300448776 /* TVCMainWindowTextViewAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524220EB673E00448776 /* TVCMainWindowTextViewAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559620EB6CB300448776 /* TVCMemberList.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516920EB673E00448776 /* TVCMemberList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559720EB6CB300448776 /* TVCMemberListAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525120EB673E00448776 /* TVCMemberListAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559820EB6CB300448776 /* TVCServerList.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516720EB673E00448776 /* TVCServerList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559920EB6CB300448776 /* TVCServerListAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516D20EB673E00448776 /* TVCServerListAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559A20EB6CB300448776 /* TVCTextViewWithIRCFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524420EB673E00448776 /* TVCTextViewWithIRCFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559B20EB6CB300448776 /* TVCValidatedComboBox.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525020EB673E00448776 /* TVCValidatedComboBox.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559C20EB6CB300448776 /* TVCValidatedTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525E20EB673E00448776 /* TVCValidatedTextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559D20EB6CB300448776 /* TXAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517320EB673E00448776 /* TXAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559E20EB6CB300448776 /* TXAppearanceHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517420EB673E00448776 /* TXAppearanceHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C31559F20EB6CB300448776 /* TXGlobalModels.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524820EB673E00448776 /* TXGlobalModels.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A020EB6CB300448776 /* TXMasterController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517A20EB673E00448776 /* TXMasterController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A120EB6CB300448776 /* TXMenuController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519B20EB673E00448776 /* TXMenuController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A220EB6CB300448776 /* TXSharedApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516B20EB673E00448776 /* TXSharedApplication.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A320EB6CB400448776 /* IRC.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524120EB673E00448776 /* IRC.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A420EB6CB400448776 /* IRCAddressBook.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A720EB673E00448776 /* IRCAddressBook.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A520EB6CB400448776 /* IRCAddressBookUserTracking.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517620EB673E00448776 /* IRCAddressBookUserTracking.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A620EB6CB400448776 /* IRCChannel.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524B20EB673E00448776 /* IRCChannel.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A720EB6CB400448776 /* IRCChannelConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A420EB673E00448776 /* IRCChannelConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A820EB6CB400448776 /* IRCChannelMode.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519A20EB673E00448776 /* IRCChannelMode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155A920EB6CB400448776 /* IRCChannelUser.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517920EB673E00448776 /* IRCChannelUser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155AA20EB6CB400448776 /* IRCClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517C20EB673E00448776 /* IRCClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155AB20EB6CB400448776 /* IRCClientConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517F20EB673E00448776 /* IRCClientConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155AC20EB6CB400448776 /* IRCColorFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31526120EB673E00448776 /* IRCColorFormat.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155AD20EB6CB400448776 /* IRCCommandIndex.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517120EB673E00448776 /* IRCCommandIndex.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155AE20EB6CB400448776 /* IRCConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31526020EB673E00448776 /* IRCConnection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155AF20EB6CB400448776 /* IRCHighlightLogEntry.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A120EB673E00448776 /* IRCHighlightLogEntry.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B020EB6CB400448776 /* IRCHighlightMatchCondition.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524720EB673E00448776 /* IRCHighlightMatchCondition.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B120EB6CB400448776 /* IRCISupportInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519820EB673E00448776 /* IRCISupportInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B220EB6CB400448776 /* IRCMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525320EB673E00448776 /* IRCMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B320EB6CB400448776 /* IRCModeInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516C20EB673E00448776 /* IRCModeInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B420EB6CB400448776 /* IRCNetworkList.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518120EB673E00448776 /* IRCNetworkList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B520EB6CB400448776 /* IRCNumerics.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516620EB673E00448776 /* IRCNumerics.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B620EB6CB400448776 /* IRCPrefix.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A520EB673E00448776 /* IRCPrefix.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B720EB6CB400448776 /* IRCSendingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525520EB673E00448776 /* IRCSendingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B820EB6CB400448776 /* IRCServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524620EB673E00448776 /* IRCServer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155B920EB6CB400448776 /* IRCTreeItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519720EB673E00448776 /* IRCTreeItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155BA20EB6CB400448776 /* IRCUser.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524E20EB673E00448776 /* IRCUser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155BB20EB6CB400448776 /* IRCUserRelations.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524F20EB673E00448776 /* IRCUserRelations.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155BC20EB6CB400448776 /* IRCWorld.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518020EB673E00448776 /* IRCWorld.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155BD20EB6CB400448776 /* NSColorHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519E20EB673E00448776 /* NSColorHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155BE20EB6CB400448776 /* NSStringHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525820EB673E00448776 /* NSStringHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155BF20EB6CB400448776 /* NSViewHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524320EB673E00448776 /* NSViewHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C020EB6CB400448776 /* TDCAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524920EB673E00448776 /* TDCAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C120EB6CB400448776 /* TDCInputPrompt.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516A20EB673E00448776 /* TDCInputPrompt.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C220EB6CB400448776 /* TDCSheetBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518220EB673E00448776 /* TDCSheetBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C320EB6CB400448776 /* TDCWindowBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525C20EB673E00448776 /* TDCWindowBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C420EB6CB400448776 /* Textual.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525F20EB673E00448776 /* Textual.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C520EB6CB400448776 /* TextualApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517720EB673E00448776 /* TextualApplication.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C620EB6CB400448776 /* THOPluginProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517820EB673E00448776 /* THOPluginProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C720EB6CB400448776 /* THOUnicodeHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A620EB673E00448776 /* THOUnicodeHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C820EB6CB400448776 /* TLOEncryptionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524520EB673E00448776 /* TLOEncryptionManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155C920EB6CB400448776 /* TLONotificationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151AA20EB673E00448776 /* TLONotificationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155CA20EB6CB400448776 /* TLOInternetAddressLookup.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525B20EB673E00448776 /* TLOInternetAddressLookup.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155CB20EB6CB400448776 /* TLOKeyEventHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A820EB673E00448776 /* TLOKeyEventHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155CC20EB6CB400448776 /* TLOLinkParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519D20EB673E00448776 /* TLOLinkParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155CD20EB6CB400448776 /* TLOpenLink.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517B20EB673E00448776 /* TLOpenLink.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155CF20EB6CB400448776 /* TLOSoundPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516420EB673E00448776 /* TLOSoundPlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155D020EB6CB400448776 /* TPCApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524020EB673E00448776 /* TPCApplicationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155D120EB6CB400448776 /* TPCPathInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517D20EB673E00448776 /* TPCPathInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155D420EB6CB400448776 /* TPCPreferencesImportExport.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524A20EB673E00448776 /* TPCPreferencesImportExport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155D520EB6CB400448776 /* TPCPreferencesLocal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516820EB673E00448776 /* TPCPreferencesLocal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155D620EB6CB400448776 /* TPCPreferencesReload.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519C20EB673E00448776 /* TPCPreferencesReload.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155D720EB6CB400448776 /* TPCPreferencesUserDefaultsLocal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519F20EB673E00448776 /* TPCPreferencesUserDefaultsLocal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155D820EB6CB400448776 /* TPCResourceManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516520EB673E00448776 /* TPCResourceManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155D920EB6CB400448776 /* TPCThemeController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517220EB673E00448776 /* TPCThemeController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155DA20EB6CB400448776 /* TPCTheme.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517020EB673E00448776 /* TPCTheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155DB20EB6CB400448776 /* TVCAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516F20EB673E00448776 /* TVCAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155DC20EB6CB400448776 /* TVCAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516E20EB673E00448776 /* TVCAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155DD20EB6CB400448776 /* TVCAutoExpandingTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517E20EB673E00448776 /* TVCAutoExpandingTextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155DE20EB6CB400448776 /* TVCAutoExpandingTokenField.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A320EB673E00448776 /* TVCAutoExpandingTokenField.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155DF20EB6CB400448776 /* TVCBasicTableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A220EB673E00448776 /* TVCBasicTableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E020EB6CB400448776 /* TVCChannelSelectionViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519620EB673E00448776 /* TVCChannelSelectionViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E120EB6CB400448776 /* TVCErrorMessagePopoverControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517520EB673E00448776 /* TVCErrorMessagePopoverControllerPrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E220EB6CB400448776 /* TVCErrorMessagePopoverPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151A020EB673E00448776 /* TVCErrorMessagePopoverPrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E320EB6CB400448776 /* TVCLogController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525620EB673E00448776 /* TVCLogController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E420EB6CB400448776 /* TVCLogLine.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519920EB673E00448776 /* TVCLogLine.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E520EB6CB400448776 /* TVCLogRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525420EB673E00448776 /* TVCLogRenderer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E620EB6CB400448776 /* TVCLogView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524D20EB673E00448776 /* TVCLogView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E720EB6CB400448776 /* TVCMainWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524C20EB673E00448776 /* TVCMainWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E820EB6CB400448776 /* TVCMainWindowAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525A20EB673E00448776 /* TVCMainWindowAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155E920EB6CB400448776 /* TVCMainWindowLoadingScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525720EB673E00448776 /* TVCMainWindowLoadingScreen.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155EA20EB6CB400448776 /* TVCMainWindowSplitView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525220EB673E00448776 /* TVCMainWindowSplitView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155EB20EB6CB400448776 /* TVCMainWindowTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525920EB673E00448776 /* TVCMainWindowTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155EC20EB6CB400448776 /* TVCMainWindowTextViewAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524220EB673E00448776 /* TVCMainWindowTextViewAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155ED20EB6CB400448776 /* TVCMemberList.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516920EB673E00448776 /* TVCMemberList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155EE20EB6CB400448776 /* TVCMemberListAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525120EB673E00448776 /* TVCMemberListAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155EF20EB6CB400448776 /* TVCServerList.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516720EB673E00448776 /* TVCServerList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F020EB6CB400448776 /* TVCServerListAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516D20EB673E00448776 /* TVCServerListAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F120EB6CB400448776 /* TVCTextViewWithIRCFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524420EB673E00448776 /* TVCTextViewWithIRCFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F220EB6CB400448776 /* TVCValidatedComboBox.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525020EB673E00448776 /* TVCValidatedComboBox.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F320EB6CB400448776 /* TVCValidatedTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31525E20EB673E00448776 /* TVCValidatedTextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F420EB6CB400448776 /* TXAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517320EB673E00448776 /* TXAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F520EB6CB400448776 /* TXAppearanceHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517420EB673E00448776 /* TXAppearanceHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F620EB6CB400448776 /* TXGlobalModels.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31524820EB673E00448776 /* TXGlobalModels.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F720EB6CB400448776 /* TXMasterController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31517A20EB673E00448776 /* TXMasterController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F820EB6CB400448776 /* TXMenuController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519B20EB673E00448776 /* TXMenuController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3155F920EB6CB400448776 /* TXSharedApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516B20EB673E00448776 /* TXSharedApplication.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C3156E420EB6D0500448776 /* WebScriptObjectHelperPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151AD20EB673E00448776 /* WebScriptObjectHelperPrivate.h */; }; 4C3156E620EB6D0500448776 /* IRCAddressBookMatchCachePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E020EB673E00448776 /* IRCAddressBookMatchCachePrivate.h */; }; 4C3156E720EB6D0500448776 /* IRCAddressBookUserTrackingPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151EA20EB673E00448776 /* IRCAddressBookUserTrackingPrivate.h */; }; 4C3156E820EB6D0500448776 /* IRCChannelConfigPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522120EB673E00448776 /* IRCChannelConfigPrivate.h */; }; 4C3156E920EB6D0500448776 /* IRCChannelModePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522920EB673E00448776 /* IRCChannelModePrivate.h */; }; 4C3156EA20EB6D0500448776 /* IRCChannelPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520220EB673E00448776 /* IRCChannelPrivate.h */; }; 4C3156EB20EB6D0500448776 /* IRCChannelUserPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521120EB673E00448776 /* IRCChannelUserPrivate.h */; }; 4C3156EC20EB6D0500448776 /* IRCClientConfigPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C720EB673E00448776 /* IRCClientConfigPrivate.h */; }; 4C3156ED20EB6D0500448776 /* IRCClientPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520B20EB673E00448776 /* IRCClientPrivate.h */; }; 4C3156EE20EB6D0500448776 /* IRCClientRequestedCommandsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523F20EB673E00448776 /* IRCClientRequestedCommandsPrivate.h */; }; 4C3156EF20EB6D0500448776 /* IRCColorFormatPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B120EB673E00448776 /* IRCColorFormatPrivate.h */; }; 4C3156F020EB6D0500448776 /* IRCCommandIndexPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151AE20EB673E00448776 /* IRCCommandIndexPrivate.h */; }; 4C3156F120EB6D0500448776 /* IRCConnectionPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522020EB673E00448776 /* IRCConnectionPrivate.h */; }; 4C3156F220EB6D0500448776 /* IRCExtrasPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F620EB673E00448776 /* IRCExtrasPrivate.h */; }; 4C3156F320EB6D0500448776 /* IRCHighlightLogEntryPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B220EB673E00448776 /* IRCHighlightLogEntryPrivate.h */; }; 4C3156F420EB6D0500448776 /* IRCISupportInfoPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151ED20EB673E00448776 /* IRCISupportInfoPrivate.h */; }; 4C3156F520EB6D0500448776 /* IRCMessageBatchPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E120EB673E00448776 /* IRCMessageBatchPrivate.h */; }; 4C3156F620EB6D0500448776 /* IRCMessagePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BE20EB673E00448776 /* IRCMessagePrivate.h */; }; 4C3156F720EB6D0500448776 /* IRCServerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C120EB673E00448776 /* IRCServerPrivate.h */; }; 4C3156F820EB6D0500448776 /* IRCTimerCommandPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B820EB673E00448776 /* IRCTimerCommandPrivate.h */; }; 4C3156F920EB6D0500448776 /* IRCTreeItemPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E320EB673E00448776 /* IRCTreeItemPrivate.h */; }; 4C3156FA20EB6D0500448776 /* IRCUserNicknameColorStyleGeneratorPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151FF20EB673E00448776 /* IRCUserNicknameColorStyleGeneratorPrivate.h */; }; 4C3156FB20EB6D0500448776 /* IRCUserPersistentStorePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BC20EB673E00448776 /* IRCUserPersistentStorePrivate.h */; }; 4C3156FC20EB6D0500448776 /* IRCUserPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E720EB673E00448776 /* IRCUserPrivate.h */; }; 4C3156FD20EB6D0500448776 /* IRCUserRelationsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B420EB673E00448776 /* IRCUserRelationsPrivate.h */; }; 4C3156FE20EB6D0500448776 /* IRCWorldPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DB20EB673E00448776 /* IRCWorldPrivate.h */; }; 4C31570020EB6D0500448776 /* NSTableVIewHelperPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E220EB673E00448776 /* NSTableVIewHelperPrivate.h */; }; 4C31570120EB6D0500448776 /* NSViewHelperPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521420EB673E00448776 /* NSViewHelperPrivate.h */; }; 4C31570220EB6D0500448776 /* SwiftBridgingHeaderPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520A20EB673E00448776 /* SwiftBridgingHeaderPrivate.h */; }; 4C31570320EB6D0500448776 /* TDCAboutDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522820EB673E00448776 /* TDCAboutDialogPrivate.h */; }; 4C31570420EB6D0500448776 /* TDCAddressBookSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BA20EB673E00448776 /* TDCAddressBookSheetPrivate.h */; }; 4C31570920EB6D0500448776 /* TDCChannelBanListSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B920EB673E00448776 /* TDCChannelBanListSheetPrivate.h */; }; 4C31570A20EB6D0500448776 /* TDCChannelInviteSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523E20EB673E00448776 /* TDCChannelInviteSheetPrivate.h */; }; 4C31570B20EB6D0500448776 /* TDCChannelModifyModesSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523D20EB673E00448776 /* TDCChannelModifyModesSheetPrivate.h */; }; 4C31570C20EB6D0500448776 /* TDCChannelModifyTopicSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E820EB673E00448776 /* TDCChannelModifyTopicSheetPrivate.h */; }; 4C31570D20EB6D0500448776 /* TDCChannelPropertiesNotificationConfigurationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B520EB673E00448776 /* TDCChannelPropertiesNotificationConfigurationPrivate.h */; }; 4C31570E20EB6D0500448776 /* TDCChannelPropertiesSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523B20EB673E00448776 /* TDCChannelPropertiesSheetPrivate.h */; }; 4C31570F20EB6D0500448776 /* TDCChannelSpotlightAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521B20EB673E00448776 /* TDCChannelSpotlightAppearancePrivate.h */; }; 4C31571020EB6D0500448776 /* TDCChannelSpotlightControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522B20EB673E00448776 /* TDCChannelSpotlightControllerPrivate.h */; }; 4C31571120EB6D0500448776 /* TDCChannelSpotlightControlsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D120EB673E00448776 /* TDCChannelSpotlightControlsPrivate.h */; }; 4C31571220EB6D0500448776 /* TDCChannelSpotlightSearchResultPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D920EB673E00448776 /* TDCChannelSpotlightSearchResultPrivate.h */; }; 4C31571320EB6D0500448776 /* TDCChannelSpotlightSearchResultsTablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522C20EB673E00448776 /* TDCChannelSpotlightSearchResultsTablePrivate.h */; }; 4C31571420EB6D0500448776 /* TDCFileTransferDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521220EB673E00448776 /* TDCFileTransferDialogPrivate.h */; }; 4C31571520EB6D0500448776 /* TDCFileTransferDialogTableCellPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BD20EB673E00448776 /* TDCFileTransferDialogTableCellPrivate.h */; }; 4C31571620EB6D0500448776 /* TDCFileTransferDialogTransferControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520720EB673E00448776 /* TDCFileTransferDialogTransferControllerPrivate.h */; }; 4C31571720EB6D0500448776 /* TDCHighlightEntrySheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D720EB673E00448776 /* TDCHighlightEntrySheetPrivate.h */; }; 4C31571C20EB6D0500448776 /* TDCLicenseManagerDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523720EB673E00448776 /* TDCLicenseManagerDialogPrivate.h */; }; 4C31571D20EB6D0500448776 /* TDCLicenseManagerMigrateAppStoreSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151FD20EB673E00448776 /* TDCLicenseManagerMigrateAppStoreSheetPrivate.h */; }; 4C31571E20EB6D0500448776 /* TDCLicenseManagerRecoverLostLicenseSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523C20EB673E00448776 /* TDCLicenseManagerRecoverLostLicenseSheetPrivate.h */; }; 4C31571F20EB6D0500448776 /* TDCLicenseUpgradeActivateSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520E20EB673E00448776 /* TDCLicenseUpgradeActivateSheetPrivate.h */; }; 4C31572020EB6D0500448776 /* TDCLicenseUpgradeCommonActionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D320EB673E00448776 /* TDCLicenseUpgradeCommonActionsPrivate.h */; }; 4C31572120EB6D0500448776 /* TDCLicenseUpgradeDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CC20EB673E00448776 /* TDCLicenseUpgradeDialogPrivate.h */; }; 4C31572220EB6D0500448776 /* TDCLicenseUpgradeEligibilitySheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151FC20EB673E00448776 /* TDCLicenseUpgradeEligibilitySheetPrivate.h */; }; 4C31572320EB6D0500448776 /* TDCNicknameColorSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E620EB673E00448776 /* TDCNicknameColorSheetPrivate.h */; }; 4C31572420EB6D0500448776 /* TDCPreferencesControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F120EB673E00448776 /* TDCPreferencesControllerPrivate.h */; }; 4C31572520EB6D0500448776 /* TDCPreferencesNotificationConfigurationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523920EB673E00448776 /* TDCPreferencesNotificationConfigurationPrivate.h */; }; 4C31572620EB6D0500448776 /* TDCProgressIndicatorSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DC20EB673E00448776 /* TDCProgressIndicatorSheetPrivate.h */; }; 4C31572720EB6D0500448776 /* TDCServerChangeNicknameSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151EF20EB673E00448776 /* TDCServerChangeNicknameSheetPrivate.h */; }; 4C31572820EB6D0500448776 /* TDCServerChannelListDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C320EB673E00448776 /* TDCServerChannelListDialogPrivate.h */; }; 4C31572920EB6D0500448776 /* TDCServerEndpointListSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522F20EB673E00448776 /* TDCServerEndpointListSheetPrivate.h */; }; 4C31572A20EB6D0500448776 /* TDCServerEndpointListSheetTablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151EB20EB673E00448776 /* TDCServerEndpointListSheetTablePrivate.h */; }; 4C31572B20EB6D0500448776 /* TDCServerHighlightListSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520F20EB673E00448776 /* TDCServerHighlightListSheetPrivate.h */; }; 4C31572C20EB6D0500448776 /* TDCServerPropertiesSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E920EB673E00448776 /* TDCServerPropertiesSheetPrivate.h */; }; 4C31572D20EB6D0500448776 /* TDCSharedProtocolDefinitionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151FB20EB673E00448776 /* TDCSharedProtocolDefinitionsPrivate.h */; }; 4C31572E20EB6D0500448776 /* TDCWelcomeSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C420EB673E00448776 /* TDCWelcomeSheetPrivate.h */; }; 4C31572F20EB6D0500448776 /* TextualPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D820EB673E00448776 /* TextualPrivate.h */; }; 4C31573020EB6D0500448776 /* THOPluginDispatcherPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521620EB673E00448776 /* THOPluginDispatcherPrivate.h */; }; 4C31573120EB6D0500448776 /* THOPluginItemPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521920EB673E00448776 /* THOPluginItemPrivate.h */; }; 4C31573220EB6D0500448776 /* THOPluginManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CF20EB673E00448776 /* THOPluginManagerPrivate.h */; }; 4C31573320EB6D0500448776 /* THOPluginProtocolPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B320EB673E00448776 /* THOPluginProtocolPrivate.h */; }; 4C31573520EB6D0500448776 /* TLOEncryptionManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520120EB673E00448776 /* TLOEncryptionManagerPrivate.h */; }; 4C31573620EB6D0500448776 /* TLOFileLoggerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F220EB673E00448776 /* TLOFileLoggerPrivate.h */; }; 4C31573720EB6D0500448776 /* TLONotificationControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151AF20EB673E00448776 /* TLONotificationControllerPrivate.h */; }; 4C31573820EB6D0500448776 /* TLOInputHistoryPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DA20EB673E00448776 /* TLOInputHistoryPrivate.h */; }; 4C31573920EB6D0500448776 /* TLOLicenseManagerDownloaderPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520920EB673E00448776 /* TLOLicenseManagerDownloaderPrivate.h */; }; 4C31573A20EB6D0500448776 /* TLOLicenseManagerLastGenPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523320EB673E00448776 /* TLOLicenseManagerLastGenPrivate.h */; }; 4C31573B20EB6D0500448776 /* TLOLicenseManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522220EB673E00448776 /* TLOLicenseManagerPrivate.h */; }; 4C31573C20EB6D0500448776 /* TLONicknameCompletionStatusPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523520EB673E00448776 /* TLONicknameCompletionStatusPrivate.h */; }; 4C31573D20EB6D0500448776 /* TLONotificationConfigurationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523420EB673E00448776 /* TLONotificationConfigurationPrivate.h */; }; 4C31573E20EB6D0500448776 /* TLOSpeechSynthesizerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523620EB673E00448776 /* TLOSpeechSynthesizerPrivate.h */; }; 4C31573F20EB6D0500448776 /* TLOSpokenNotificationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520320EB673E00448776 /* TLOSpokenNotificationPrivate.h */; }; 4C31574020EB6D0500448776 /* TPCApplicationInfoPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151EE20EB673E00448776 /* TPCApplicationInfoPrivate.h */; }; 4C31574120EB6D0500448776 /* TPCPathInfoPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D220EB673E00448776 /* TPCPathInfoPrivate.h */; }; 4C31574420EB6D0500448776 /* TPCPreferencesImportExportPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E420EB673E00448776 /* TPCPreferencesImportExportPrivate.h */; }; 4C31574520EB6D0500448776 /* TPCPreferencesLocalPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521820EB673E00448776 /* TPCPreferencesLocalPrivate.h */; }; 4C31574820EB6D0500448776 /* TPCResourceManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522520EB673E00448776 /* TPCResourceManagerPrivate.h */; }; 4C31574920EB6D0500448776 /* TPCThemeControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521020EB673E00448776 /* TPCThemeControllerPrivate.h */; }; 4C31574A20EB6D0500448776 /* TPCThemePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D620EB673E00448776 /* TPCThemePrivate.h */; }; 4C31574B20EB6D0500448776 /* TVCAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D520EB673E00448776 /* TVCAppearancePrivate.h */; }; 4C31574C20EB6D0500448776 /* TVCChannelSelectionOutlineViewCellPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F820EB673E00448776 /* TVCChannelSelectionOutlineViewCellPrivate.h */; }; 4C31574D20EB6D0500448776 /* TVCChannelSelectionViewControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520520EB673E00448776 /* TVCChannelSelectionViewControllerPrivate.h */; }; 4C31574E20EB6D0500448776 /* TVCContentNavigationOutlineViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521F20EB673E00448776 /* TVCContentNavigationOutlineViewPrivate.h */; }; 4C31574F20EB6D0500448776 /* TVCDockIconPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522720EB673E00448776 /* TVCDockIconPrivate.h */; }; 4C31575020EB6D0500448776 /* TVCLogControllerHistoricLogFilePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522420EB673E00448776 /* TVCLogControllerHistoricLogFilePrivate.h */; }; 4C31575120EB6D0500448776 /* TVCLogControllerInlineMediaServicePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C220EB673E00448776 /* TVCLogControllerInlineMediaServicePrivate.h */; }; 4C31575220EB6D0500448776 /* TVCLogControllerOperationQueuePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C920EB673E00448776 /* TVCLogControllerOperationQueuePrivate.h */; }; 4C31575320EB6D0500448776 /* TVCLogControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521520EB673E00448776 /* TVCLogControllerPrivate.h */; }; 4C31575420EB6D0500448776 /* TVCLogLinePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523020EB673E00448776 /* TVCLogLinePrivate.h */; }; 4C31575520EB6D0500448776 /* TVCLogPolicyPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DD20EB673E00448776 /* TVCLogPolicyPrivate.h */; }; 4C31575620EB6D0500448776 /* TVCLogScriptEventSinkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B020EB673E00448776 /* TVCLogScriptEventSinkPrivate.h */; }; 4C31575720EB6D0500448776 /* TVCLogViewInternalWK1.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521D20EB673E00448776 /* TVCLogViewInternalWK1.h */; }; 4C31575820EB6D0500448776 /* TVCLogViewInternalWK2.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F020EB673E00448776 /* TVCLogViewInternalWK2.h */; }; 4C31575920EB6D0500448776 /* TVCLogViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520D20EB673E00448776 /* TVCLogViewPrivate.h */; }; 4C31575A20EB6D0500448776 /* TVCMainWindowAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520020EB673E00448776 /* TVCMainWindowAppearancePrivate.h */; }; 4C31575B20EB6D0500448776 /* TVCMainWindowChannelViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C820EB673E00448776 /* TVCMainWindowChannelViewPrivate.h */; }; 4C31575C20EB6D0500448776 /* TVCMainWindowLoadingScreenPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F420EB673E00448776 /* TVCMainWindowLoadingScreenPrivate.h */; }; 4C31575D20EB6D0500448776 /* TVCMainWindowPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523820EB673E00448776 /* TVCMainWindowPrivate.h */; }; 4C31575E20EB6D0500448776 /* TVCMainWindowSegmentedControlPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522D20EB673E00448776 /* TVCMainWindowSegmentedControlPrivate.h */; }; 4C31576020EB6D0500448776 /* TVCMainWindowSplitViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CE20EB673E00448776 /* TVCMainWindowSplitViewPrivate.h */; }; 4C31576120EB6D0500448776 /* TVCMainWindowTextViewAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F320EB673E00448776 /* TVCMainWindowTextViewAppearancePrivate.h */; }; 4C31576220EB6D0500448776 /* TVCMainWindowTextViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BB20EB673E00448776 /* TVCMainWindowTextViewPrivate.h */; }; 4C31576320EB6D0500448776 /* TVCMainWindowTitlebarAccessoryViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DF20EB673E00448776 /* TVCMainWindowTitlebarAccessoryViewPrivate.h */; }; 4C31576420EB6D0500448776 /* TVCMemberListAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521A20EB673E00448776 /* TVCMemberListAppearancePrivate.h */; }; 4C31576520EB6D0500448776 /* TVCMemberListCellPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C520EB673E00448776 /* TVCMemberListCellPrivate.h */; }; 4C31576620EB6D0500448776 /* TVCMemberListPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BF20EB673E00448776 /* TVCMemberListPrivate.h */; }; 4C31576720EB6D0500448776 /* TVCMemberListUserInfoPopoverPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B620EB673E00448776 /* TVCMemberListUserInfoPopoverPrivate.h */; }; 4C31576820EB6D0500448776 /* TVCNotificationConfigurationViewControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522320EB673E00448776 /* TVCNotificationConfigurationViewControllerPrivate.h */; }; 4C31576920EB6D0500448776 /* TVCServerListAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521720EB673E00448776 /* TVCServerListAppearancePrivate.h */; }; 4C31576A20EB6D0500448776 /* TVCServerListCellPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F720EB673E00448776 /* TVCServerListCellPrivate.h */; }; 4C31576B20EB6D0500448776 /* TVCServerListPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CA20EB673E00448776 /* TVCServerListPrivate.h */; }; 4C31576C20EB6D0500448776 /* TVCTextFormatterMenuPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522620EB673E00448776 /* TVCTextFormatterMenuPrivate.h */; }; 4C31576D20EB6D0500448776 /* TVCTextViewWithIRCFormatterPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520820EB673E00448776 /* TVCTextViewWithIRCFormatterPrivate.h */; }; 4C31576E20EB6D0500448776 /* TVCWK1AutoScrollerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CD20EB673E00448776 /* TVCWK1AutoScrollerPrivate.h */; }; 4C31576F20EB6D0500448776 /* TXAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523A20EB673E00448776 /* TXAppearancePrivate.h */; }; 4C31577020EB6D0500448776 /* TXApplicationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520620EB673E00448776 /* TXApplicationPrivate.h */; }; 4C31577120EB6D0500448776 /* TXGlobalModelsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523220EB673E00448776 /* TXGlobalModelsPrivate.h */; }; 4C31577220EB6D0500448776 /* TXMasterControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520420EB673E00448776 /* TXMasterControllerPrivate.h */; }; 4C31577320EB6D0500448776 /* TXMenuControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DE20EB673E00448776 /* TXMenuControllerPrivate.h */; }; 4C31577420EB6D0500448776 /* TXSharedApplicationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C620EB673E00448776 /* TXSharedApplicationPrivate.h */; }; 4C31577520EB6D0500448776 /* TXWindowControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522A20EB673E00448776 /* TXWindowControllerPrivate.h */; }; 4C31577620EB6D0500448776 /* WKWebViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523120EB673E00448776 /* WKWebViewPrivate.h */; }; 4C31577720EB6D0600448776 /* WebScriptObjectHelperPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151AD20EB673E00448776 /* WebScriptObjectHelperPrivate.h */; }; 4C31577920EB6D0600448776 /* IRCAddressBookMatchCachePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E020EB673E00448776 /* IRCAddressBookMatchCachePrivate.h */; }; 4C31577A20EB6D0600448776 /* IRCAddressBookUserTrackingPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151EA20EB673E00448776 /* IRCAddressBookUserTrackingPrivate.h */; }; 4C31577B20EB6D0600448776 /* IRCChannelConfigPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522120EB673E00448776 /* IRCChannelConfigPrivate.h */; }; 4C31577C20EB6D0600448776 /* IRCChannelModePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522920EB673E00448776 /* IRCChannelModePrivate.h */; }; 4C31577D20EB6D0600448776 /* IRCChannelPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520220EB673E00448776 /* IRCChannelPrivate.h */; }; 4C31577E20EB6D0600448776 /* IRCChannelUserPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521120EB673E00448776 /* IRCChannelUserPrivate.h */; }; 4C31577F20EB6D0600448776 /* IRCClientConfigPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C720EB673E00448776 /* IRCClientConfigPrivate.h */; }; 4C31578020EB6D0600448776 /* IRCClientPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520B20EB673E00448776 /* IRCClientPrivate.h */; }; 4C31578120EB6D0600448776 /* IRCClientRequestedCommandsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523F20EB673E00448776 /* IRCClientRequestedCommandsPrivate.h */; }; 4C31578220EB6D0600448776 /* IRCColorFormatPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B120EB673E00448776 /* IRCColorFormatPrivate.h */; }; 4C31578320EB6D0600448776 /* IRCCommandIndexPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151AE20EB673E00448776 /* IRCCommandIndexPrivate.h */; }; 4C31578420EB6D0600448776 /* IRCConnectionPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522020EB673E00448776 /* IRCConnectionPrivate.h */; }; 4C31578520EB6D0600448776 /* IRCExtrasPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F620EB673E00448776 /* IRCExtrasPrivate.h */; }; 4C31578620EB6D0600448776 /* IRCHighlightLogEntryPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B220EB673E00448776 /* IRCHighlightLogEntryPrivate.h */; }; 4C31578720EB6D0600448776 /* IRCISupportInfoPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151ED20EB673E00448776 /* IRCISupportInfoPrivate.h */; }; 4C31578820EB6D0600448776 /* IRCMessageBatchPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E120EB673E00448776 /* IRCMessageBatchPrivate.h */; }; 4C31578920EB6D0600448776 /* IRCMessagePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BE20EB673E00448776 /* IRCMessagePrivate.h */; }; 4C31578A20EB6D0600448776 /* IRCServerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C120EB673E00448776 /* IRCServerPrivate.h */; }; 4C31578B20EB6D0600448776 /* IRCTimerCommandPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B820EB673E00448776 /* IRCTimerCommandPrivate.h */; }; 4C31578C20EB6D0600448776 /* IRCTreeItemPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E320EB673E00448776 /* IRCTreeItemPrivate.h */; }; 4C31578D20EB6D0600448776 /* IRCUserNicknameColorStyleGeneratorPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151FF20EB673E00448776 /* IRCUserNicknameColorStyleGeneratorPrivate.h */; }; 4C31578E20EB6D0600448776 /* IRCUserPersistentStorePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BC20EB673E00448776 /* IRCUserPersistentStorePrivate.h */; }; 4C31578F20EB6D0600448776 /* IRCUserPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E720EB673E00448776 /* IRCUserPrivate.h */; }; 4C31579020EB6D0600448776 /* IRCUserRelationsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B420EB673E00448776 /* IRCUserRelationsPrivate.h */; }; 4C31579120EB6D0600448776 /* IRCWorldPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DB20EB673E00448776 /* IRCWorldPrivate.h */; }; 4C31579320EB6D0600448776 /* NSTableVIewHelperPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E220EB673E00448776 /* NSTableVIewHelperPrivate.h */; }; 4C31579420EB6D0600448776 /* NSViewHelperPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521420EB673E00448776 /* NSViewHelperPrivate.h */; }; 4C31579520EB6D0600448776 /* SwiftBridgingHeaderPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520A20EB673E00448776 /* SwiftBridgingHeaderPrivate.h */; }; 4C31579620EB6D0600448776 /* TDCAboutDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522820EB673E00448776 /* TDCAboutDialogPrivate.h */; }; 4C31579720EB6D0600448776 /* TDCAddressBookSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BA20EB673E00448776 /* TDCAddressBookSheetPrivate.h */; }; 4C31579C20EB6D0600448776 /* TDCChannelBanListSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B920EB673E00448776 /* TDCChannelBanListSheetPrivate.h */; }; 4C31579D20EB6D0600448776 /* TDCChannelInviteSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523E20EB673E00448776 /* TDCChannelInviteSheetPrivate.h */; }; 4C31579E20EB6D0600448776 /* TDCChannelModifyModesSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523D20EB673E00448776 /* TDCChannelModifyModesSheetPrivate.h */; }; 4C31579F20EB6D0600448776 /* TDCChannelModifyTopicSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E820EB673E00448776 /* TDCChannelModifyTopicSheetPrivate.h */; }; 4C3157A020EB6D0600448776 /* TDCChannelPropertiesNotificationConfigurationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B520EB673E00448776 /* TDCChannelPropertiesNotificationConfigurationPrivate.h */; }; 4C3157A120EB6D0600448776 /* TDCChannelPropertiesSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523B20EB673E00448776 /* TDCChannelPropertiesSheetPrivate.h */; }; 4C3157A220EB6D0600448776 /* TDCChannelSpotlightAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521B20EB673E00448776 /* TDCChannelSpotlightAppearancePrivate.h */; }; 4C3157A320EB6D0600448776 /* TDCChannelSpotlightControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522B20EB673E00448776 /* TDCChannelSpotlightControllerPrivate.h */; }; 4C3157A420EB6D0600448776 /* TDCChannelSpotlightControlsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D120EB673E00448776 /* TDCChannelSpotlightControlsPrivate.h */; }; 4C3157A520EB6D0600448776 /* TDCChannelSpotlightSearchResultPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D920EB673E00448776 /* TDCChannelSpotlightSearchResultPrivate.h */; }; 4C3157A620EB6D0600448776 /* TDCChannelSpotlightSearchResultsTablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522C20EB673E00448776 /* TDCChannelSpotlightSearchResultsTablePrivate.h */; }; 4C3157A720EB6D0600448776 /* TDCFileTransferDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521220EB673E00448776 /* TDCFileTransferDialogPrivate.h */; }; 4C3157A820EB6D0600448776 /* TDCFileTransferDialogTableCellPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BD20EB673E00448776 /* TDCFileTransferDialogTableCellPrivate.h */; }; 4C3157A920EB6D0600448776 /* TDCFileTransferDialogTransferControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520720EB673E00448776 /* TDCFileTransferDialogTransferControllerPrivate.h */; }; 4C3157AA20EB6D0600448776 /* TDCHighlightEntrySheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D720EB673E00448776 /* TDCHighlightEntrySheetPrivate.h */; }; 4C3157AF20EB6D0600448776 /* TDCLicenseManagerDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523720EB673E00448776 /* TDCLicenseManagerDialogPrivate.h */; }; 4C3157B020EB6D0600448776 /* TDCLicenseManagerMigrateAppStoreSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151FD20EB673E00448776 /* TDCLicenseManagerMigrateAppStoreSheetPrivate.h */; }; 4C3157B120EB6D0600448776 /* TDCLicenseManagerRecoverLostLicenseSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523C20EB673E00448776 /* TDCLicenseManagerRecoverLostLicenseSheetPrivate.h */; }; 4C3157B220EB6D0600448776 /* TDCLicenseUpgradeActivateSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520E20EB673E00448776 /* TDCLicenseUpgradeActivateSheetPrivate.h */; }; 4C3157B320EB6D0600448776 /* TDCLicenseUpgradeCommonActionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D320EB673E00448776 /* TDCLicenseUpgradeCommonActionsPrivate.h */; }; 4C3157B420EB6D0600448776 /* TDCLicenseUpgradeDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CC20EB673E00448776 /* TDCLicenseUpgradeDialogPrivate.h */; }; 4C3157B520EB6D0600448776 /* TDCLicenseUpgradeEligibilitySheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151FC20EB673E00448776 /* TDCLicenseUpgradeEligibilitySheetPrivate.h */; }; 4C3157B620EB6D0600448776 /* TDCNicknameColorSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E620EB673E00448776 /* TDCNicknameColorSheetPrivate.h */; }; 4C3157B720EB6D0600448776 /* TDCPreferencesControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F120EB673E00448776 /* TDCPreferencesControllerPrivate.h */; }; 4C3157B820EB6D0600448776 /* TDCPreferencesNotificationConfigurationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523920EB673E00448776 /* TDCPreferencesNotificationConfigurationPrivate.h */; }; 4C3157B920EB6D0600448776 /* TDCProgressIndicatorSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DC20EB673E00448776 /* TDCProgressIndicatorSheetPrivate.h */; }; 4C3157BA20EB6D0600448776 /* TDCServerChangeNicknameSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151EF20EB673E00448776 /* TDCServerChangeNicknameSheetPrivate.h */; }; 4C3157BB20EB6D0600448776 /* TDCServerChannelListDialogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C320EB673E00448776 /* TDCServerChannelListDialogPrivate.h */; }; 4C3157BC20EB6D0600448776 /* TDCServerEndpointListSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522F20EB673E00448776 /* TDCServerEndpointListSheetPrivate.h */; }; 4C3157BD20EB6D0600448776 /* TDCServerEndpointListSheetTablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151EB20EB673E00448776 /* TDCServerEndpointListSheetTablePrivate.h */; }; 4C3157BE20EB6D0600448776 /* TDCServerHighlightListSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520F20EB673E00448776 /* TDCServerHighlightListSheetPrivate.h */; }; 4C3157BF20EB6D0600448776 /* TDCServerPropertiesSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E920EB673E00448776 /* TDCServerPropertiesSheetPrivate.h */; }; 4C3157C020EB6D0600448776 /* TDCSharedProtocolDefinitionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151FB20EB673E00448776 /* TDCSharedProtocolDefinitionsPrivate.h */; }; 4C3157C120EB6D0600448776 /* TDCWelcomeSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C420EB673E00448776 /* TDCWelcomeSheetPrivate.h */; }; 4C3157C220EB6D0600448776 /* TextualPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D820EB673E00448776 /* TextualPrivate.h */; }; 4C3157C320EB6D0600448776 /* THOPluginDispatcherPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521620EB673E00448776 /* THOPluginDispatcherPrivate.h */; }; 4C3157C420EB6D0600448776 /* THOPluginItemPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521920EB673E00448776 /* THOPluginItemPrivate.h */; }; 4C3157C520EB6D0600448776 /* THOPluginManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CF20EB673E00448776 /* THOPluginManagerPrivate.h */; }; 4C3157C620EB6D0600448776 /* THOPluginProtocolPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B320EB673E00448776 /* THOPluginProtocolPrivate.h */; }; 4C3157C820EB6D0600448776 /* TLOEncryptionManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520120EB673E00448776 /* TLOEncryptionManagerPrivate.h */; }; 4C3157C920EB6D0600448776 /* TLOFileLoggerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F220EB673E00448776 /* TLOFileLoggerPrivate.h */; }; 4C3157CA20EB6D0600448776 /* TLONotificationControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151AF20EB673E00448776 /* TLONotificationControllerPrivate.h */; }; 4C3157CB20EB6D0600448776 /* TLOInputHistoryPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DA20EB673E00448776 /* TLOInputHistoryPrivate.h */; }; 4C3157CC20EB6D0600448776 /* TLOLicenseManagerDownloaderPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520920EB673E00448776 /* TLOLicenseManagerDownloaderPrivate.h */; }; 4C3157CD20EB6D0600448776 /* TLOLicenseManagerLastGenPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523320EB673E00448776 /* TLOLicenseManagerLastGenPrivate.h */; }; 4C3157CE20EB6D0600448776 /* TLOLicenseManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522220EB673E00448776 /* TLOLicenseManagerPrivate.h */; }; 4C3157CF20EB6D0600448776 /* TLONicknameCompletionStatusPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523520EB673E00448776 /* TLONicknameCompletionStatusPrivate.h */; }; 4C3157D020EB6D0600448776 /* TLONotificationConfigurationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523420EB673E00448776 /* TLONotificationConfigurationPrivate.h */; }; 4C3157D120EB6D0600448776 /* TLOSpeechSynthesizerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523620EB673E00448776 /* TLOSpeechSynthesizerPrivate.h */; }; 4C3157D220EB6D0600448776 /* TLOSpokenNotificationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520320EB673E00448776 /* TLOSpokenNotificationPrivate.h */; }; 4C3157D320EB6D0600448776 /* TPCApplicationInfoPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151EE20EB673E00448776 /* TPCApplicationInfoPrivate.h */; }; 4C3157D420EB6D0600448776 /* TPCPathInfoPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D220EB673E00448776 /* TPCPathInfoPrivate.h */; }; 4C3157D720EB6D0600448776 /* TPCPreferencesImportExportPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151E420EB673E00448776 /* TPCPreferencesImportExportPrivate.h */; }; 4C3157D820EB6D0600448776 /* TPCPreferencesLocalPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521820EB673E00448776 /* TPCPreferencesLocalPrivate.h */; }; 4C3157DB20EB6D0600448776 /* TPCResourceManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522520EB673E00448776 /* TPCResourceManagerPrivate.h */; }; 4C3157DC20EB6D0600448776 /* TPCThemeControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521020EB673E00448776 /* TPCThemeControllerPrivate.h */; }; 4C3157DD20EB6D0600448776 /* TPCThemePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D620EB673E00448776 /* TPCThemePrivate.h */; }; 4C3157DE20EB6D0600448776 /* TVCAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151D520EB673E00448776 /* TVCAppearancePrivate.h */; }; 4C3157DF20EB6D0600448776 /* TVCChannelSelectionOutlineViewCellPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F820EB673E00448776 /* TVCChannelSelectionOutlineViewCellPrivate.h */; }; 4C3157E020EB6D0600448776 /* TVCChannelSelectionViewControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520520EB673E00448776 /* TVCChannelSelectionViewControllerPrivate.h */; }; 4C3157E120EB6D0600448776 /* TVCContentNavigationOutlineViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521F20EB673E00448776 /* TVCContentNavigationOutlineViewPrivate.h */; }; 4C3157E220EB6D0600448776 /* TVCDockIconPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522720EB673E00448776 /* TVCDockIconPrivate.h */; }; 4C3157E320EB6D0600448776 /* TVCLogControllerHistoricLogFilePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522420EB673E00448776 /* TVCLogControllerHistoricLogFilePrivate.h */; }; 4C3157E420EB6D0600448776 /* TVCLogControllerInlineMediaServicePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C220EB673E00448776 /* TVCLogControllerInlineMediaServicePrivate.h */; }; 4C3157E520EB6D0600448776 /* TVCLogControllerOperationQueuePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C920EB673E00448776 /* TVCLogControllerOperationQueuePrivate.h */; }; 4C3157E620EB6D0600448776 /* TVCLogControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521520EB673E00448776 /* TVCLogControllerPrivate.h */; }; 4C3157E720EB6D0600448776 /* TVCLogLinePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523020EB673E00448776 /* TVCLogLinePrivate.h */; }; 4C3157E820EB6D0600448776 /* TVCLogPolicyPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DD20EB673E00448776 /* TVCLogPolicyPrivate.h */; }; 4C3157E920EB6D0600448776 /* TVCLogScriptEventSinkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B020EB673E00448776 /* TVCLogScriptEventSinkPrivate.h */; }; 4C3157EA20EB6D0600448776 /* TVCLogViewInternalWK1.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521D20EB673E00448776 /* TVCLogViewInternalWK1.h */; }; 4C3157EB20EB6D0600448776 /* TVCLogViewInternalWK2.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F020EB673E00448776 /* TVCLogViewInternalWK2.h */; }; 4C3157EC20EB6D0600448776 /* TVCLogViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520D20EB673E00448776 /* TVCLogViewPrivate.h */; }; 4C3157ED20EB6D0600448776 /* TVCMainWindowAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520020EB673E00448776 /* TVCMainWindowAppearancePrivate.h */; }; 4C3157EE20EB6D0600448776 /* TVCMainWindowChannelViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C820EB673E00448776 /* TVCMainWindowChannelViewPrivate.h */; }; 4C3157EF20EB6D0600448776 /* TVCMainWindowLoadingScreenPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F420EB673E00448776 /* TVCMainWindowLoadingScreenPrivate.h */; }; 4C3157F020EB6D0600448776 /* TVCMainWindowPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523820EB673E00448776 /* TVCMainWindowPrivate.h */; }; 4C3157F120EB6D0600448776 /* TVCMainWindowSegmentedControlPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522D20EB673E00448776 /* TVCMainWindowSegmentedControlPrivate.h */; }; 4C3157F320EB6D0600448776 /* TVCMainWindowSplitViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CE20EB673E00448776 /* TVCMainWindowSplitViewPrivate.h */; }; 4C3157F420EB6D0600448776 /* TVCMainWindowTextViewAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F320EB673E00448776 /* TVCMainWindowTextViewAppearancePrivate.h */; }; 4C3157F520EB6D0600448776 /* TVCMainWindowTextViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BB20EB673E00448776 /* TVCMainWindowTextViewPrivate.h */; }; 4C3157F620EB6D0600448776 /* TVCMainWindowTitlebarAccessoryViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DF20EB673E00448776 /* TVCMainWindowTitlebarAccessoryViewPrivate.h */; }; 4C3157F720EB6D0600448776 /* TVCMemberListAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521A20EB673E00448776 /* TVCMemberListAppearancePrivate.h */; }; 4C3157F820EB6D0600448776 /* TVCMemberListCellPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C520EB673E00448776 /* TVCMemberListCellPrivate.h */; }; 4C3157F920EB6D0600448776 /* TVCMemberListPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151BF20EB673E00448776 /* TVCMemberListPrivate.h */; }; 4C3157FA20EB6D0600448776 /* TVCMemberListUserInfoPopoverPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151B620EB673E00448776 /* TVCMemberListUserInfoPopoverPrivate.h */; }; 4C3157FB20EB6D0600448776 /* TVCNotificationConfigurationViewControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522320EB673E00448776 /* TVCNotificationConfigurationViewControllerPrivate.h */; }; 4C3157FC20EB6D0600448776 /* TVCServerListAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31521720EB673E00448776 /* TVCServerListAppearancePrivate.h */; }; 4C3157FD20EB6D0600448776 /* TVCServerListCellPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151F720EB673E00448776 /* TVCServerListCellPrivate.h */; }; 4C3157FE20EB6D0600448776 /* TVCServerListPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CA20EB673E00448776 /* TVCServerListPrivate.h */; }; 4C3157FF20EB6D0600448776 /* TVCTextFormatterMenuPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522620EB673E00448776 /* TVCTextFormatterMenuPrivate.h */; }; 4C31580020EB6D0600448776 /* TVCTextViewWithIRCFormatterPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520820EB673E00448776 /* TVCTextViewWithIRCFormatterPrivate.h */; }; 4C31580120EB6D0600448776 /* TVCWK1AutoScrollerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151CD20EB673E00448776 /* TVCWK1AutoScrollerPrivate.h */; }; 4C31580220EB6D0600448776 /* TXAppearancePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523A20EB673E00448776 /* TXAppearancePrivate.h */; }; 4C31580320EB6D0600448776 /* TXApplicationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520620EB673E00448776 /* TXApplicationPrivate.h */; }; 4C31580420EB6D0600448776 /* TXGlobalModelsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523220EB673E00448776 /* TXGlobalModelsPrivate.h */; }; 4C31580520EB6D0600448776 /* TXMasterControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31520420EB673E00448776 /* TXMasterControllerPrivate.h */; }; 4C31580620EB6D0600448776 /* TXMenuControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151DE20EB673E00448776 /* TXMenuControllerPrivate.h */; }; 4C31580720EB6D0600448776 /* TXSharedApplicationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C3151C620EB673E00448776 /* TXSharedApplicationPrivate.h */; }; 4C31580820EB6D0600448776 /* TXWindowControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31522A20EB673E00448776 /* TXWindowControllerPrivate.h */; }; 4C31580920EB6D0600448776 /* WKWebViewPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31523120EB673E00448776 /* WKWebViewPrivate.h */; }; 4C3158AE20EB6D0F00448776 /* IRCServerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518520EB673E00448776 /* IRCServerInternal.h */; }; 4C3158AF20EB6D0F00448776 /* IRCUserInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518620EB673E00448776 /* IRCUserInternal.h */; }; 4C3158B020EB6D0F00448776 /* IRCAddressBookInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518720EB673E00448776 /* IRCAddressBookInternal.h */; }; 4C3158B120EB6D0F00448776 /* TDCChannelPropertiesSheetInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518820EB673E00448776 /* TDCChannelPropertiesSheetInternal.h */; }; 4C3158B220EB6D0F00448776 /* TDCChannelSpotlightAppearanceInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518920EB673E00448776 /* TDCChannelSpotlightAppearanceInternal.h */; }; 4C3158B420EB6D0F00448776 /* IRCPrefixInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518B20EB673E00448776 /* IRCPrefixInternal.h */; }; 4C3158B520EB6D0F00448776 /* IRCChannelConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518C20EB673E00448776 /* IRCChannelConfigInternal.h */; }; 4C3158B620EB6D0F00448776 /* IRCClientConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518D20EB673E00448776 /* IRCClientConfigInternal.h */; }; 4C3158B720EB6D0F00448776 /* TVCLogLineInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518E20EB673E00448776 /* TVCLogLineInternal.h */; }; 4C3158B820EB6D0F00448776 /* IRCChannelUserInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518F20EB673E00448776 /* IRCChannelUserInternal.h */; }; 4C3158B920EB6D0F00448776 /* IRCModeInfoInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519020EB673E00448776 /* IRCModeInfoInternal.h */; }; 4C3158BA20EB6D0F00448776 /* IRCMessageInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519120EB673E00448776 /* IRCMessageInternal.h */; }; 4C3158BB20EB6D0F00448776 /* TDCFileTransferDialogInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519220EB673E00448776 /* TDCFileTransferDialogInternal.h */; }; 4C3158BC20EB6D0F00448776 /* TDCChannelSpotlightControllerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519320EB673E00448776 /* TDCChannelSpotlightControllerInternal.h */; }; 4C3158BD20EB6D0F00448776 /* IRCHighlightLogEntryInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519420EB673E00448776 /* IRCHighlightLogEntryInternal.h */; }; 4C3158BE20EB6D0F00448776 /* IRCHighlightMatchConditionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519520EB673E00448776 /* IRCHighlightMatchConditionInternal.h */; }; 4C3158BF20EB6D1000448776 /* IRCServerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518520EB673E00448776 /* IRCServerInternal.h */; }; 4C3158C020EB6D1000448776 /* IRCUserInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518620EB673E00448776 /* IRCUserInternal.h */; }; 4C3158C120EB6D1000448776 /* IRCAddressBookInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518720EB673E00448776 /* IRCAddressBookInternal.h */; }; 4C3158C220EB6D1000448776 /* TDCChannelPropertiesSheetInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518820EB673E00448776 /* TDCChannelPropertiesSheetInternal.h */; }; 4C3158C320EB6D1000448776 /* TDCChannelSpotlightAppearanceInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518920EB673E00448776 /* TDCChannelSpotlightAppearanceInternal.h */; }; 4C3158C520EB6D1000448776 /* IRCPrefixInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518B20EB673E00448776 /* IRCPrefixInternal.h */; }; 4C3158C620EB6D1000448776 /* IRCChannelConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518C20EB673E00448776 /* IRCChannelConfigInternal.h */; }; 4C3158C720EB6D1000448776 /* IRCClientConfigInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518D20EB673E00448776 /* IRCClientConfigInternal.h */; }; 4C3158C820EB6D1000448776 /* TVCLogLineInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518E20EB673E00448776 /* TVCLogLineInternal.h */; }; 4C3158C920EB6D1000448776 /* IRCChannelUserInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31518F20EB673E00448776 /* IRCChannelUserInternal.h */; }; 4C3158CA20EB6D1000448776 /* IRCModeInfoInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519020EB673E00448776 /* IRCModeInfoInternal.h */; }; 4C3158CB20EB6D1000448776 /* IRCMessageInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519120EB673E00448776 /* IRCMessageInternal.h */; }; 4C3158CC20EB6D1000448776 /* TDCFileTransferDialogInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519220EB673E00448776 /* TDCFileTransferDialogInternal.h */; }; 4C3158CD20EB6D1000448776 /* TDCChannelSpotlightControllerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519320EB673E00448776 /* TDCChannelSpotlightControllerInternal.h */; }; 4C3158CE20EB6D1000448776 /* IRCHighlightLogEntryInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519420EB673E00448776 /* IRCHighlightLogEntryInternal.h */; }; 4C3158CF20EB6D1000448776 /* IRCHighlightMatchConditionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31519520EB673E00448776 /* IRCHighlightMatchConditionInternal.h */; }; 4C3158E320EB6D1600448776 /* GTMEncodeHTML.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516220EB673E00448776 /* GTMEncodeHTML.h */; }; 4C3158E420EB6D1600448776 /* OELReachability.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516320EB673E00448776 /* OELReachability.h */; }; 4C3158E520EB6D1600448776 /* GTMEncodeHTML.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516220EB673E00448776 /* GTMEncodeHTML.h */; }; 4C3158E620EB6D1600448776 /* OELReachability.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C31516320EB673E00448776 /* OELReachability.h */; }; 4C33341D20CC471A00ACB9AD /* Bundled Scripts in Resources */ = {isa = PBXBuildFile; fileRef = 4C33341B20CC471A00ACB9AD /* Bundled Scripts */; }; 4C33341E20CC471A00ACB9AD /* Bundled Scripts in Resources */ = {isa = PBXBuildFile; fileRef = 4C33341B20CC471A00ACB9AD /* Bundled Scripts */; }; 4C33342220CC475300ACB9AD /* Bundled Styles in Resources */ = {isa = PBXBuildFile; fileRef = 4C33342020CC475300ACB9AD /* Bundled Styles */; }; 4C33342320CC475300ACB9AD /* Bundled Styles in Resources */ = {isa = PBXBuildFile; fileRef = 4C33342020CC475300ACB9AD /* Bundled Styles */; }; 4C469E3B20EC5BDB00094EA4 /* TLOLinkParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515520EB673E00448776 /* TLOLinkParser.swift */; }; 4C469E3C20EC5BDB00094EA4 /* TLOpenLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515920EB673E00448776 /* TLOpenLink.swift */; }; 4C469E3D20EC5BDB00094EA4 /* TLOLinkParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515520EB673E00448776 /* TLOLinkParser.swift */; }; 4C469E3E20EC5BDB00094EA4 /* TLOpenLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C31515920EB673E00448776 /* TLOpenLink.swift */; }; 4C51327920F76E980033B703 /* TLOLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C51327720F76E980033B703 /* TLOLocalization.swift */; }; 4C51327A20F76E980033B703 /* TLOLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C51327720F76E980033B703 /* TLOLocalization.swift */; }; 4C52379415C18F6700414852 /* Style Default Templates in Resources */ = {isa = PBXBuildFile; fileRef = 4C52379215C18F6700414852 /* Style Default Templates */; }; 4C5274E220F8D78800B18F9D /* IRCConnectionErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C5274E020F8D78700B18F9D /* IRCConnectionErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C5274E320F8D78800B18F9D /* IRCConnectionErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C5274E020F8D78700B18F9D /* IRCConnectionErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C5274E720F8D7CD00B18F9D /* IRCConnectionErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C5274E520F8D7CD00B18F9D /* IRCConnectionErrors.m */; }; 4C5274E820F8D7CD00B18F9D /* IRCConnectionErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C5274E520F8D7CD00B18F9D /* IRCConnectionErrors.m */; }; 4C58B572186011B900834882 /* Acknowledgements.pdf in Copy Acknowledgements */ = {isa = PBXBuildFile; fileRef = 4C58B570186011AC00834882 /* Acknowledgements.pdf */; }; 4C58B573186011BE00834882 /* Acknowledgements.pdf in Copy Acknowledgements */ = {isa = PBXBuildFile; fileRef = 4C58B570186011AC00834882 /* Acknowledgements.pdf */; }; 4C5BA3CB16F1302F00A96CA2 /* Style Default Templates in Resources */ = {isa = PBXBuildFile; fileRef = 4C52379215C18F6700414852 /* Style Default Templates */; }; 4C7B05001D1210F300295E82 /* BuildConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CC70202171E1CB400BDFAE0 /* BuildConfig.h */; }; 4C7B05011D12110500295E82 /* BuildConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CC70202171E1CB400BDFAE0 /* BuildConfig.h */; }; 4C83B9121BA9BED500BD7718 /* Chat Filters.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C83B9101BA9BEB800BD7718 /* Chat Filters.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4C8878042C33706E0016DB98 /* TPCSandboxMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C8878032C33706E0016DB98 /* TPCSandboxMigration.m */; }; 4C8878052C33706E0016DB98 /* TPCSandboxMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C8878032C33706E0016DB98 /* TPCSandboxMigration.m */; }; 4C8878072C3371470016DB98 /* TPCSandboxMigrationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C8878062C3371470016DB98 /* TPCSandboxMigrationPrivate.h */; }; 4C8878082C3371470016DB98 /* TPCSandboxMigrationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C8878062C3371470016DB98 /* TPCSandboxMigrationPrivate.h */; }; 4C8F3FD82C31AF7700118AAF /* THOPluginManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C8F3FD62C31AF6E00118AAF /* THOPluginManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C8F3FD92C31AF7700118AAF /* THOPluginManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C8F3FD62C31AF6E00118AAF /* THOPluginManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C8F3FDC2C31B15B00118AAF /* THOPluginItemLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C8F3FDB2C31B15200118AAF /* THOPluginItemLogging.m */; }; 4C8F3FDD2C31B15B00118AAF /* THOPluginItemLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C8F3FDB2C31B15200118AAF /* THOPluginItemLogging.m */; }; 4C95689F194A894D003D97FD /* Smiley Converter.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C956893194A8862003D97FD /* Smiley Converter.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4C9A1D9C1836E6D300C14835 /* User Insights.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C9A1D921836E6A100C14835 /* User Insights.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4C9A1D9D1836E6D500C14835 /* System Info.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C9A1D931836E6A100C14835 /* System Info.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4C9A1D9E1836E6D700C14835 /* ZNC Additions.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C9A1D941836E6A100C14835 /* ZNC Additions.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 4CAB27522537EE44009B1F07 /* IRCChannelMemberList.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAB27502537EE43009B1F07 /* IRCChannelMemberList.m */; }; 4CAB27532537EE44009B1F07 /* IRCChannelMemberList.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAB27502537EE43009B1F07 /* IRCChannelMemberList.m */; }; 4CAB27582537EE66009B1F07 /* IRCChannelMemberListPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CAB27562537EE66009B1F07 /* IRCChannelMemberListPrivate.h */; }; 4CAB27592537EE66009B1F07 /* IRCChannelMemberListPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CAB27562537EE66009B1F07 /* IRCChannelMemberListPrivate.h */; }; 4CAB275F2537F71B009B1F07 /* IRCChannelMemberList.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CAB275D2537F71B009B1F07 /* IRCChannelMemberList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4CAB27602537F71B009B1F07 /* IRCChannelMemberList.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CAB275D2537F71B009B1F07 /* IRCChannelMemberList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4CAB2766253863BF009B1F07 /* IRCChannelMemberListController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAB2764253863BF009B1F07 /* IRCChannelMemberListController.m */; }; 4CAB2767253863BF009B1F07 /* IRCChannelMemberListController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CAB2764253863BF009B1F07 /* IRCChannelMemberListController.m */; }; 4CAB276C253863D9009B1F07 /* IRCChannelMemberListControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CAB276A253863D9009B1F07 /* IRCChannelMemberListControllerPrivate.h */; }; 4CAB276D253863D9009B1F07 /* IRCChannelMemberListControllerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CAB276A253863D9009B1F07 /* IRCChannelMemberListControllerPrivate.h */; }; 4CAC9D7B1F2B076100AD5F12 /* Textual-Extras.pkg in Resources */ = {isa = PBXBuildFile; fileRef = 4C2F0C701F2AF3860017D3B7 /* Textual-Extras.pkg */; }; 4CAC9D831F2B076400AD5F12 /* Textual-Extras.pkg in Resources */ = {isa = PBXBuildFile; fileRef = 4C2F0C701F2AF3860017D3B7 /* Textual-Extras.pkg */; }; 4CB638CC2D45A06C002B2CC3 /* FeatureFlags.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CB638CB2D45A06C002B2CC3 /* FeatureFlags.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4CB638CD2D45A06C002B2CC3 /* FeatureFlags.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CB638CB2D45A06C002B2CC3 /* FeatureFlags.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4CB873DD22B4A56B005AB046 /* TDCPreferencesUserStyleSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CB873DB22B4A56B005AB046 /* TDCPreferencesUserStyleSheet.m */; }; 4CB873DE22B4A56B005AB046 /* TDCPreferencesUserStyleSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CB873DB22B4A56B005AB046 /* TDCPreferencesUserStyleSheet.m */; }; 4CCD93FE1CA7291300335381 /* JavaScript in Resources */ = {isa = PBXBuildFile; fileRef = 4CCD93FC1CA7291300335381 /* JavaScript */; }; 4CCD93FF1CA7291300335381 /* JavaScript in Resources */ = {isa = PBXBuildFile; fileRef = 4CCD93FC1CA7291300335381 /* JavaScript */; }; 4CEA6AA52C36F90000A36551 /* KeysExcludedFromExport.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C315EE72C36581A001A16B4 /* KeysExcludedFromExport.plist */; }; 4CEA6AA62C36F90000A36551 /* PreferenceKeyMasterList.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C315EE82C36581A001A16B4 /* PreferenceKeyMasterList.plist */; }; 4CEA6AA72C36F90000A36551 /* RegisteredUserDefaults.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A520EB676400448776 /* RegisteredUserDefaults.plist */; }; 4CEA6AA82C36F90000A36551 /* RegisteredUserDefaultsInContainer.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A620EB676400448776 /* RegisteredUserDefaultsInContainer.plist */; }; 4CEA6AAA2C36F91E00A36551 /* KeysExcludedFromExport.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C315EE72C36581A001A16B4 /* KeysExcludedFromExport.plist */; }; 4CEA6AAB2C36F91E00A36551 /* PreferenceKeyMasterList.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C315EE82C36581A001A16B4 /* PreferenceKeyMasterList.plist */; }; 4CEA6AAC2C36F91E00A36551 /* RegisteredUserDefaults.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A520EB676400448776 /* RegisteredUserDefaults.plist */; }; 4CEA6AAD2C36F91E00A36551 /* RegisteredUserDefaultsInContainer.plist in Copy Preference Resources */ = {isa = PBXBuildFile; fileRef = 4C3153A620EB676400448776 /* RegisteredUserDefaultsInContainer.plist */; }; 4CEE0BDE268F3C9B004CB3C3 /* Chat Filters.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C83B9101BA9BEB800BD7718 /* Chat Filters.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4CEE0BDF268F3C9B004CB3C3 /* Smiley Converter.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C956893194A8862003D97FD /* Smiley Converter.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4CEE0BE0268F3C9B004CB3C3 /* User Insights.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C9A1D921836E6A100C14835 /* User Insights.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4CEE0BE1268F3C9B004CB3C3 /* System Info.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C9A1D931836E6A100C14835 /* System Info.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4CEE0BE2268F3C9B004CB3C3 /* ZNC Additions.bundle in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C9A1D941836E6A100C14835 /* ZNC Additions.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4CEFEDAB22B4AF42002CEE19 /* TDCPreferencesUserStyleSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CB873D622B4A497005AB046 /* TDCPreferencesUserStyleSheetPrivate.h */; }; 4CEFEDAC22B4AF42002CEE19 /* TDCPreferencesUserStyleSheetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CB873D622B4A497005AB046 /* TDCPreferencesUserStyleSheetPrivate.h */; }; 4CEFEDB122B4B6E8002CEE19 /* TDCPreferencesUserStyleSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4CEFEDAE22B4B6E8002CEE19 /* TDCPreferencesUserStyleSheet.strings */; }; 4CEFEDB222B4B6E8002CEE19 /* TDCPreferencesUserStyleSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4CEFEDAE22B4B6E8002CEE19 /* TDCPreferencesUserStyleSheet.strings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 4CBB842C16F0DA1D004E3ED6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4C1DC5491580420500A47BC9 /* Project object */; proxyType = 1; remoteGlobalIDString = 4CCF301015804DCE006FFE21; remoteInfo = "Build Frameworks"; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 4C06E44320EC539E0055D09A /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 4C06E43620EC539E0055D09A /* AutoHyperlinks.framework in Embed Frameworks */, 4C06E43820EC539E0055D09A /* CocoaExtensions.framework in Embed Frameworks */, 4C06E43A20EC539E0055D09A /* EncryptionKit.framework in Embed Frameworks */, 4C06E43C20EC539E0055D09A /* GRMustache.framework in Embed Frameworks */, 4C06E44220EC539E0055D09A /* Sparkle.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; 4C06E48020EC541C0055D09A /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 4C06E47320EC541C0055D09A /* AutoHyperlinks.framework in Embed Frameworks */, 4C06E47520EC541C0055D09A /* CocoaExtensions.framework in Embed Frameworks */, 4C06E47720EC541C0055D09A /* EncryptionKit.framework in Embed Frameworks */, 4C06E47920EC541C0055D09A /* GRMustache.framework in Embed Frameworks */, 4C06E47F20EC541C0055D09A /* Sparkle.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; 4C52AED515811C2000940619 /* Copy Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 12; dstPath = "Bundled Extensions"; dstSubfolderSpec = 7; files = ( 4C2E92212C1E446E003BB92D /* Caffeine.bundle in Copy Extensions */, 4C83B9121BA9BED500BD7718 /* Chat Filters.bundle in Copy Extensions */, 4C9A1D9C1836E6D300C14835 /* User Insights.bundle in Copy Extensions */, 4C9A1D9D1836E6D500C14835 /* System Info.bundle in Copy Extensions */, 4C95689F194A894D003D97FD /* Smiley Converter.bundle in Copy Extensions */, 4C9A1D9E1836E6D700C14835 /* ZNC Additions.bundle in Copy Extensions */, ); name = "Copy Extensions"; runOnlyForDeploymentPostprocessing = 0; }; 4C5BA4DC16F1302F00A96CA2 /* Copy Acknowledgements */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Documentation; dstSubfolderSpec = 7; files = ( 4C58B573186011BE00834882 /* Acknowledgements.pdf in Copy Acknowledgements */, ); name = "Copy Acknowledgements"; runOnlyForDeploymentPostprocessing = 0; }; 4C761BA41E155C4300A505B7 /* Copy XPC Services */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; dstSubfolderSpec = 16; files = ( 4C17167D254A58BD004BE8AE /* Inline Content Loader.xpc in Copy XPC Services */, 4C17167E254A58BD004BE8AE /* IRC Connection Host.xpc in Copy XPC Services */, 4C17167F254A58BD004BE8AE /* Scrollback History Manager.xpc in Copy XPC Services */, ); name = "Copy XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; 4C761BA61E155C6E00A505B7 /* Copy XPC Services */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; dstSubfolderSpec = 16; files = ( 4C171681254A58C2004BE8AE /* Inline Content Loader.xpc in Copy XPC Services */, 4C171682254A58C2004BE8AE /* IRC Connection Host.xpc in Copy XPC Services */, 4C171683254A58C2004BE8AE /* Scrollback History Manager.xpc in Copy XPC Services */, ); name = "Copy XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; 4C9E534415B4C171000E84A9 /* Copy Acknowledgements */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Documentation; dstSubfolderSpec = 7; files = ( 4C58B572186011B900834882 /* Acknowledgements.pdf in Copy Acknowledgements */, ); name = "Copy Acknowledgements"; runOnlyForDeploymentPostprocessing = 0; }; 4CEA6AA42C36F8E700A36551 /* Copy Preference Resources */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Preferences; dstSubfolderSpec = 7; files = ( 4C2BEB642C3866DF008F8F0D /* KeysExcludedFromContainer.plist in Copy Preference Resources */, 4CEA6AA52C36F90000A36551 /* KeysExcludedFromExport.plist in Copy Preference Resources */, 4C0A31152C374A5B0081EF1F /* KeysExcludedFromMigrate.plist in Copy Preference Resources */, 4CEA6AA62C36F90000A36551 /* PreferenceKeyMasterList.plist in Copy Preference Resources */, 4CEA6AA72C36F90000A36551 /* RegisteredUserDefaults.plist in Copy Preference Resources */, 4CEA6AA82C36F90000A36551 /* RegisteredUserDefaultsInContainer.plist in Copy Preference Resources */, ); name = "Copy Preference Resources"; runOnlyForDeploymentPostprocessing = 0; }; 4CEA6AA92C36F91400A36551 /* Copy Preference Resources */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Preferences; dstSubfolderSpec = 7; files = ( 4C2BEB652C3866E2008F8F0D /* KeysExcludedFromContainer.plist in Copy Preference Resources */, 4CEA6AAA2C36F91E00A36551 /* KeysExcludedFromExport.plist in Copy Preference Resources */, 4C0A31162C374A610081EF1F /* KeysExcludedFromMigrate.plist in Copy Preference Resources */, 4CEA6AAB2C36F91E00A36551 /* PreferenceKeyMasterList.plist in Copy Preference Resources */, 4CEA6AAC2C36F91E00A36551 /* RegisteredUserDefaults.plist in Copy Preference Resources */, 4CEA6AAD2C36F91E00A36551 /* RegisteredUserDefaultsInContainer.plist in Copy Preference Resources */, ); name = "Copy Preference Resources"; runOnlyForDeploymentPostprocessing = 0; }; 4CEE0BDC268F3C8D004CB3C3 /* Copy Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "Bundled Extensions"; dstSubfolderSpec = 7; files = ( 4C2E92232C1E447E003BB92D /* Caffeine.bundle in Copy Extensions */, 4CEE0BDE268F3C9B004CB3C3 /* Chat Filters.bundle in Copy Extensions */, 4CEE0BDF268F3C9B004CB3C3 /* Smiley Converter.bundle in Copy Extensions */, 4CEE0BE0268F3C9B004CB3C3 /* User Insights.bundle in Copy Extensions */, 4CEE0BE1268F3C9B004CB3C3 /* System Info.bundle in Copy Extensions */, 4CEE0BE2268F3C9B004CB3C3 /* ZNC Additions.bundle in Copy Extensions */, ); name = "Copy Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 4C06E13A20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCServerPropertiesSheet.xib; sourceTree = ""; }; 4C06E13E20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCServerHighlightListSheet.xib; sourceTree = ""; }; 4C06E14020EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCServerEndpointListSheet.xib; sourceTree = ""; }; 4C06E14220EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCChannelModifyModesSheet.xib; sourceTree = ""; }; 4C06E14420EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCChannelSpotlightController.xib; sourceTree = ""; }; 4C06E14620EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCFileTransferDialog.xib; sourceTree = ""; }; 4C06E14820EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCServerChannelListDialog.xib; sourceTree = ""; }; 4C06E14A20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCChannelBanListSheet.xib; sourceTree = ""; }; 4C06E14C20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCChannelPropertiesSheet.xib; sourceTree = ""; }; 4C06E14E20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCLicenseManagerMigrateAppStoreSheet.xib; sourceTree = ""; }; 4C06E15220EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCLicenseUpgradeEligibilitySheet.xib; sourceTree = ""; }; 4C06E15620EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TVCChannelSelectionView.xib; sourceTree = ""; }; 4C06E15820EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TXCMainMenu.xib; sourceTree = ""; }; 4C06E15A20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TVCNotificationConfigurationView.xib; sourceTree = ""; }; 4C06E15C20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCLicenseUpgradeActivateSheet.xib; sourceTree = ""; }; 4C06E15E20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCPreferences.xib; sourceTree = ""; }; 4C06E16020EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCChannelInviteSheet.xib; sourceTree = ""; }; 4C06E16220EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCNicknameColorSheet.xib; sourceTree = ""; }; 4C06E16420EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCWelcomeSheet.xib; sourceTree = ""; }; 4C06E16620EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCProgressIndicatorSheet.xib; sourceTree = ""; }; 4C06E16820EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCAddressBookSheet.xib; sourceTree = ""; }; 4C06E16A20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TVCAlert.xib; sourceTree = ""; }; 4C06E16C20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCLicenseUpgradeDialog.xib; sourceTree = ""; }; 4C06E16E20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCServerChangeNicknameSheet.xib; sourceTree = ""; }; 4C06E17020EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCChannelModifyTopicSheet.xib; sourceTree = ""; }; 4C06E17220EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCAboutDialog.xib; sourceTree = ""; }; 4C06E17420EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCHighlightEntrySheet.xib; sourceTree = ""; }; 4C06E17620EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TVCMainWindow.xib; sourceTree = ""; }; 4C06E17820EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCLicenseManagerDialog.xib; sourceTree = ""; }; 4C06E17A20EC4AC40055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCLicenseManagerRecoverLostLicenseSheet.xib; sourceTree = ""; }; 4C06E17C20EC4AC40055D09A /* TVCMemberListAppearance.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TVCMemberListAppearance.plist; sourceTree = ""; }; 4C06E17D20EC4AC40055D09A /* TVCServerListAppearanceTemplate.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TVCServerListAppearanceTemplate.plist; sourceTree = ""; }; 4C06E17E20EC4AC40055D09A /* TDCChannelSpotlightAppearanceTemplate.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TDCChannelSpotlightAppearanceTemplate.plist; sourceTree = ""; }; 4C06E17F20EC4AC40055D09A /* TVCMemberListAppearanceTemplate.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TVCMemberListAppearanceTemplate.plist; sourceTree = ""; }; 4C06E18020EC4AC40055D09A /* TVCMainWindowTextViewAppearanceTemplate.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TVCMainWindowTextViewAppearanceTemplate.plist; sourceTree = ""; }; 4C06E18120EC4AC40055D09A /* TDCChannelSpotlightAppearance.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TDCChannelSpotlightAppearance.plist; sourceTree = ""; }; 4C06E18220EC4AC40055D09A /* TVCMainWindowAppearance.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TVCMainWindowAppearance.plist; sourceTree = ""; }; 4C06E18320EC4AC40055D09A /* TVCMainWindowTextViewAppearance.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TVCMainWindowTextViewAppearance.plist; sourceTree = ""; }; 4C06E18420EC4AC40055D09A /* TVCServerListAppearance.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TVCServerListAppearance.plist; sourceTree = ""; }; 4C06E18520EC4AC40055D09A /* TVCMainWindowAppearanceTemplate.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TVCMainWindowAppearanceTemplate.plist; sourceTree = ""; }; 4C06E24720EC4B970055D09A /* IRCConnectionConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = IRCConnectionConfig.h; path = ../Shared/Headers/IRCConnectionConfig.h; sourceTree = SOURCE_ROOT; }; 4C06E25B20EC4BBD0055D09A /* IRCConnectionConfigInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = IRCConnectionConfigInternal.h; path = ../Shared/Headers/Internal/IRCConnectionConfigInternal.h; sourceTree = SOURCE_ROOT; }; 4C06E26020EC4C560055D09A /* TPCPreferencesPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TPCPreferencesPrivate.h; path = ../Shared/Headers/Private/TPCPreferencesPrivate.h; sourceTree = SOURCE_ROOT; }; 4C06E26120EC4C560055D09A /* TPCPreferencesUserDefaultsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TPCPreferencesUserDefaultsPrivate.h; path = ../Shared/Headers/Private/TPCPreferencesUserDefaultsPrivate.h; sourceTree = SOURCE_ROOT; }; 4C06E26220EC4C560055D09A /* NSObjectHelperPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSObjectHelperPrivate.h; path = ../Shared/Headers/Private/NSObjectHelperPrivate.h; sourceTree = SOURCE_ROOT; }; 4C06E26F20EC4E350055D09A /* StaticDefinitions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StaticDefinitions.h; path = ../Shared/Headers/StaticDefinitions.h; sourceTree = SOURCE_ROOT; }; 4C06E27420EC4E850055D09A /* TLOTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TLOTimer.h; path = ../Shared/Headers/TLOTimer.h; sourceTree = SOURCE_ROOT; }; 4C06E27520EC4E850055D09A /* TPCPreferencesUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TPCPreferencesUserDefaults.h; path = ../Shared/Headers/TPCPreferencesUserDefaults.h; sourceTree = SOURCE_ROOT; }; 4C06E27620EC4E860055D09A /* TPCPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TPCPreferences.h; path = ../Shared/Headers/TPCPreferences.h; sourceTree = SOURCE_ROOT; }; 4C06E27720EC4E860055D09A /* TLOLocalization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TLOLocalization.h; path = ../Shared/Headers/TLOLocalization.h; sourceTree = SOURCE_ROOT; }; 4C06E28820EC4F000055D09A /* TPCPreferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TPCPreferences.m; path = ../Shared/Preferences/TPCPreferences.m; sourceTree = SOURCE_ROOT; }; 4C06E28920EC4F000055D09A /* TPCPreferencesUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TPCPreferencesUserDefaults.m; path = ../Shared/Preferences/TPCPreferencesUserDefaults.m; sourceTree = SOURCE_ROOT; }; 4C06E29220EC4F570055D09A /* TLOLocalization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TLOLocalization.m; path = ../Shared/Library/TLOLocalization.m; sourceTree = SOURCE_ROOT; }; 4C06E29320EC4F570055D09A /* TLOTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TLOTimer.m; path = ../Shared/Library/TLOTimer.m; sourceTree = SOURCE_ROOT; }; 4C06E29C20EC4F880055D09A /* IRCConnectionConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = IRCConnectionConfig.m; path = ../Shared/IRC/IRCConnectionConfig.m; sourceTree = SOURCE_ROOT; }; 4C06E2A120EC4FA50055D09A /* NSObjectHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSObjectHelper.m; path = "../Shared/Helpers/Cocoa (Objective-C)/NSObjectHelper.m"; sourceTree = SOURCE_ROOT; }; 4C06E2A820EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Prompts.strings; sourceTree = ""; }; 4C06E2AA20EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/OffTheRecord.strings; sourceTree = ""; }; 4C06E2AC20EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/CommonErrors.strings; sourceTree = ""; }; 4C06E2B020EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BasicLanguage.strings; sourceTree = ""; }; 4C06E2B220EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Accessibility.strings; sourceTree = ""; }; 4C06E2B420EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCChannelPropertiesSheet.strings; sourceTree = ""; }; 4C06E2B620EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCChannelBanListSheet.strings; sourceTree = ""; }; 4C06E2B820EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCPreferencesController.strings; sourceTree = ""; }; 4C06E2BA20EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCFileTransferDialog.strings; sourceTree = ""; }; 4C06E2BC20EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCChannelInviteSheet.strings; sourceTree = ""; }; 4C06E2BE20EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCAboutDialog.strings; sourceTree = ""; }; 4C06E2C020EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCServerPropertiesSheet.strings; sourceTree = ""; }; 4C06E2C220EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/IRC.strings; sourceTree = ""; }; 4C06E2C420EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TLOLicenseManager.strings; sourceTree = ""; }; 4C06E2C620EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCChannelModifyModesSheet.strings; sourceTree = ""; }; 4C06E2C820EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TVCMainWindow.strings; sourceTree = ""; }; 4C06E2CA20EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Notifications.strings; sourceTree = ""; }; 4C06E2CE20EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TVCNotificationConfigurationView.strings; sourceTree = ""; }; 4C06E2D020EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCServerEndpointListSheet.strings; sourceTree = ""; }; 4C06E2D220EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCServerChannelListDialog.strings; sourceTree = ""; }; 4C06E2D420EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCChannelSpotlightController.strings; sourceTree = ""; }; 4C06E2D620EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCAddressBookSheet.strings; sourceTree = ""; }; 4C06E2D820EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCLicenseUpgradeEligibilitySheet.strings; sourceTree = ""; }; 4C06E2DA20EC4FF00055D09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCChannelModifyTopicSheet.strings; sourceTree = ""; }; 4C06E34E20EC50D40055D09A /* ICLPayloadLocal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLPayloadLocal.m; sourceTree = ""; }; 4C06E36320EC51370055D09A /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = ""; }; 4C06E36520EC51370055D09A /* GCDAsyncSocketExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocketExtensions.m; sourceTree = ""; }; 4C06E36B20EC51370055D09A /* GCDAsyncSocketExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocketExtensions.h; sourceTree = ""; }; 4C06E36C20EC51370055D09A /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = ""; }; 4C06E37220EC51370055D09A /* ICLPayloadInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLPayloadInternal.h; sourceTree = ""; }; 4C06E37520EC51370055D09A /* ICLPayload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLPayload.h; sourceTree = ""; }; 4C06E37F20EC51370055D09A /* RCMConnectionManagerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCMConnectionManagerProtocol.h; sourceTree = ""; }; 4C06E38020EC51370055D09A /* ICLInlineContentProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLInlineContentProtocol.h; sourceTree = ""; }; 4C06E38120EC51370055D09A /* HLSHistoricLogProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HLSHistoricLogProtocol.h; sourceTree = ""; }; 4C06E38620EC51370055D09A /* TVCLogLineXPC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TVCLogLineXPC.m; sourceTree = ""; }; 4C06E38B20EC51370055D09A /* ICLPayload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLPayload.m; sourceTree = ""; }; 4C06E41020EC52240055D09A /* TVCLogLineXPCPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TVCLogLineXPCPrivate.h; sourceTree = ""; }; 4C06E41520EC52D50055D09A /* ICLPayloadLocalPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLPayloadLocalPrivate.h; sourceTree = ""; }; 4C0A31122C37419D0081EF1F /* KeysExcludedFromMigrate.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = KeysExcludedFromMigrate.plist; sourceTree = ""; }; 4C10C00D1F37E59B0004C624 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 4C171668254A589D004BE8AE /* IRC Connection Host.xpc */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.xpc-service"; name = "IRC Connection Host.xpc"; path = "../../../.tmp/SharedBuildProducts-XPCServices/IRC Connection Host.xpc"; sourceTree = ""; }; 4C171669254A589D004BE8AE /* Inline Content Loader.xpc */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.xpc-service"; name = "Inline Content Loader.xpc"; path = "../../../.tmp/SharedBuildProducts-XPCServices/Inline Content Loader.xpc"; sourceTree = ""; }; 4C17166A254A589E004BE8AE /* Scrollback History Manager.xpc */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.xpc-service"; name = "Scrollback History Manager.xpc"; path = "../../../.tmp/SharedBuildProducts-XPCServices/Scrollback History Manager.xpc"; sourceTree = ""; }; 4C17305322B4A94000DC5836 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TDCPreferencesUserStyleSheet.xib; sourceTree = ""; }; 4C1DC5521580420500A47BC9 /* Textual.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Textual.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4C24DC751A93C9CE0098D1BE /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = "../../Frameworks/Static Libraries/Libraries/Sparkle.framework"; sourceTree = SOURCE_ROOT; }; 4C2BEB622C385189008F8F0D /* PostprocessSparkle.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = PostprocessSparkle.sh; path = "Build Scripts/PostprocessSparkle.sh"; sourceTree = ""; }; 4C2BEB632C3864C1008F8F0D /* KeysExcludedFromContainer.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = KeysExcludedFromContainer.plist; sourceTree = ""; }; 4C2E92202C1E4463003BB92D /* Caffeine.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = Caffeine.bundle; path = "../../../.tmp/SharedBuildProducts-Extensions/Caffeine.bundle"; sourceTree = ""; }; 4C2F0C701F2AF3860017D3B7 /* Textual-Extras.pkg */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Textual-Extras.pkg"; path = "Scripting/Script Files/Textual Extras Installer/Packages/Textual-Extras.pkg"; sourceTree = ""; }; 4C2F42042C3F4218000D962D /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; 4C31511420EB673E00448776 /* TPCApplicationInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCApplicationInfo.m; sourceTree = ""; }; 4C31511520EB673E00448776 /* TPCPreferencesLocal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCPreferencesLocal.m; sourceTree = ""; }; 4C31511620EB673E00448776 /* TPCPreferencesImportExport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCPreferencesImportExport.m; sourceTree = ""; }; 4C31511820EB673E00448776 /* TPCResourceManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCResourceManager.m; sourceTree = ""; }; 4C31511920EB673E00448776 /* TPCPreferencesUserDefaultsLocal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCPreferencesUserDefaultsLocal.m; sourceTree = ""; }; 4C31511A20EB673E00448776 /* TPCPathInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCPathInfo.m; sourceTree = ""; }; 4C31511F20EB673E00448776 /* TPCThemeController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCThemeController.m; sourceTree = ""; }; 4C31512020EB673E00448776 /* TPCTheme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCTheme.m; sourceTree = ""; }; 4C31512120EB673E00448776 /* TPCPreferencesReload.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCPreferencesReload.m; sourceTree = ""; }; 4C31512320EB673E00448776 /* IRCAddressBook.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCAddressBook.m; sourceTree = ""; }; 4C31512420EB673E00448776 /* IRCChannelConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCChannelConfig.m; sourceTree = ""; }; 4C31512520EB673E00448776 /* IRCPrefix.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCPrefix.m; sourceTree = ""; }; 4C31512620EB673E00448776 /* IRCHighlightLogEntry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCHighlightLogEntry.m; sourceTree = ""; }; 4C31512820EB673E00448776 /* IRCChannel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCChannel.m; sourceTree = ""; }; 4C31512920EB673E00448776 /* IRCServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCServer.m; sourceTree = ""; }; 4C31512A20EB673E00448776 /* IRCHighlightMatchCondition.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCHighlightMatchCondition.m; sourceTree = ""; }; 4C31512B20EB673E00448776 /* IRCAddressBookMatchCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCAddressBookMatchCache.m; sourceTree = ""; }; 4C31512C20EB673E00448776 /* IRCSendingMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCSendingMessage.m; sourceTree = ""; }; 4C31512D20EB673E00448776 /* IRCMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCMessage.m; sourceTree = ""; }; 4C31512E20EB673E00448776 /* IRCConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCConnection.m; sourceTree = ""; }; 4C31512F20EB673E00448776 /* IRCTimerCommand.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCTimerCommand.m; sourceTree = ""; }; 4C31513020EB673E00448776 /* IRCModeInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCModeInfo.m; sourceTree = ""; }; 4C31513120EB673E00448776 /* IRCExtras.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCExtras.m; sourceTree = ""; }; 4C31513320EB673E00448776 /* IRCUserRelations.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCUserRelations.m; sourceTree = ""; }; 4C31513420EB673E00448776 /* IRCUser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCUser.m; sourceTree = ""; }; 4C31513520EB673E00448776 /* IRCUserNicknameColorStyleGenerator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCUserNicknameColorStyleGenerator.m; sourceTree = ""; }; 4C31513620EB673E00448776 /* IRCUserPersistentStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCUserPersistentStore.m; sourceTree = ""; }; 4C31513720EB673E00448776 /* IRCCommandIndex.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCCommandIndex.m; sourceTree = ""; }; 4C31513820EB673E00448776 /* IRCClientRequestedCommands.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCClientRequestedCommands.m; sourceTree = ""; }; 4C31513920EB673E00448776 /* IRCNetworkList.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCNetworkList.m; sourceTree = ""; }; 4C31513A20EB673E00448776 /* IRCWorld.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCWorld.m; sourceTree = ""; }; 4C31513B20EB673E00448776 /* IRCClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCClient.m; sourceTree = ""; }; 4C31513C20EB673E00448776 /* IRCClientConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCClientConfig.m; sourceTree = ""; }; 4C31513D20EB673E00448776 /* IRCChannelUser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCChannelUser.m; sourceTree = ""; }; 4C31513E20EB673E00448776 /* IRCAddressBookUserTracking.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCAddressBookUserTracking.m; sourceTree = ""; }; 4C31513F20EB673E00448776 /* IRCMessageBatch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCMessageBatch.m; sourceTree = ""; }; 4C31514020EB673E00448776 /* IRCChannelMode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCChannelMode.m; sourceTree = ""; }; 4C31514120EB673E00448776 /* IRCISupportInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCISupportInfo.m; sourceTree = ""; }; 4C31514220EB673E00448776 /* IRCTreeItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCTreeItem.m; sourceTree = ""; }; 4C31514420EB673E00448776 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4C31514820EB673E00448776 /* OELReachability.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OELReachability.m; sourceTree = ""; }; 4C31514920EB673E00448776 /* TLONotificationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLONotificationController.m; sourceTree = ""; }; 4C31514B20EB673E00448776 /* TLOKeyEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOKeyEventHandler.m; sourceTree = ""; }; 4C31514E20EB673E00448776 /* TLOLicenseManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOLicenseManager.m; sourceTree = ""; }; 4C31514F20EB673E00448776 /* TLOLicenseManagerLastGen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOLicenseManagerLastGen.m; sourceTree = ""; }; 4C31515020EB673E00448776 /* TLOLicenseManagerDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOLicenseManagerDownloader.m; sourceTree = ""; }; 4C31515320EB673E00448776 /* TLOEncryptionManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOEncryptionManager.m; sourceTree = ""; }; 4C31515420EB673E00448776 /* TLOInputHistory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOInputHistory.m; sourceTree = ""; }; 4C31515520EB673E00448776 /* TLOLinkParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLOLinkParser.swift; sourceTree = ""; }; 4C31515620EB673E00448776 /* TLOInternetAddressLookup.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOInternetAddressLookup.m; sourceTree = ""; }; 4C31515720EB673E00448776 /* TLOSpeechSynthesizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOSpeechSynthesizer.m; sourceTree = ""; }; 4C31515820EB673E00448776 /* TLOSoundPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOSoundPlayer.m; sourceTree = ""; }; 4C31515920EB673E00448776 /* TLOpenLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLOpenLink.swift; sourceTree = ""; }; 4C31515A20EB673E00448776 /* TLOSpokenNotification.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOSpokenNotification.m; sourceTree = ""; }; 4C31515B20EB673E00448776 /* TLOFileLogger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLOFileLogger.m; sourceTree = ""; }; 4C31515C20EB673E00448776 /* TLONicknameCompletionStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLONicknameCompletionStatus.m; sourceTree = ""; }; 4C31515D20EB673E00448776 /* TLONotificationConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TLONotificationConfiguration.m; sourceTree = ""; }; 4C31515F20EB673E00448776 /* IRCColorFormat.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IRCColorFormat.m; sourceTree = ""; }; 4C31516220EB673E00448776 /* GTMEncodeHTML.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMEncodeHTML.h; sourceTree = ""; }; 4C31516320EB673E00448776 /* OELReachability.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OELReachability.h; sourceTree = ""; }; 4C31516420EB673E00448776 /* TLOSoundPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOSoundPlayer.h; sourceTree = ""; }; 4C31516520EB673E00448776 /* TPCResourceManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCResourceManager.h; sourceTree = ""; }; 4C31516620EB673E00448776 /* IRCNumerics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCNumerics.h; sourceTree = ""; }; 4C31516720EB673E00448776 /* TVCServerList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCServerList.h; sourceTree = ""; }; 4C31516820EB673E00448776 /* TPCPreferencesLocal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPreferencesLocal.h; sourceTree = ""; }; 4C31516920EB673E00448776 /* TVCMemberList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMemberList.h; sourceTree = ""; }; 4C31516A20EB673E00448776 /* TDCInputPrompt.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCInputPrompt.h; sourceTree = ""; }; 4C31516B20EB673E00448776 /* TXSharedApplication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXSharedApplication.h; sourceTree = ""; }; 4C31516C20EB673E00448776 /* IRCModeInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCModeInfo.h; sourceTree = ""; }; 4C31516D20EB673E00448776 /* TVCServerListAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCServerListAppearance.h; sourceTree = ""; }; 4C31516E20EB673E00448776 /* TVCAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCAppearance.h; sourceTree = ""; }; 4C31516F20EB673E00448776 /* TVCAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCAlert.h; sourceTree = ""; }; 4C31517020EB673E00448776 /* TPCTheme.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCTheme.h; sourceTree = ""; }; 4C31517120EB673E00448776 /* IRCCommandIndex.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCCommandIndex.h; sourceTree = ""; }; 4C31517220EB673E00448776 /* TPCThemeController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCThemeController.h; sourceTree = ""; }; 4C31517320EB673E00448776 /* TXAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXAppearance.h; sourceTree = ""; }; 4C31517420EB673E00448776 /* TXAppearanceHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXAppearanceHelper.h; sourceTree = ""; }; 4C31517520EB673E00448776 /* TVCErrorMessagePopoverControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCErrorMessagePopoverControllerPrivate.h; sourceTree = ""; }; 4C31517620EB673E00448776 /* IRCAddressBookUserTracking.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCAddressBookUserTracking.h; sourceTree = ""; }; 4C31517720EB673E00448776 /* TextualApplication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TextualApplication.h; sourceTree = ""; }; 4C31517820EB673E00448776 /* THOPluginProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = THOPluginProtocol.h; sourceTree = ""; }; 4C31517920EB673E00448776 /* IRCChannelUser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelUser.h; sourceTree = ""; }; 4C31517A20EB673E00448776 /* TXMasterController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXMasterController.h; sourceTree = ""; }; 4C31517B20EB673E00448776 /* TLOpenLink.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOpenLink.h; sourceTree = ""; }; 4C31517C20EB673E00448776 /* IRCClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCClient.h; sourceTree = ""; }; 4C31517D20EB673E00448776 /* TPCPathInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPathInfo.h; sourceTree = ""; }; 4C31517E20EB673E00448776 /* TVCAutoExpandingTextField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCAutoExpandingTextField.h; sourceTree = ""; }; 4C31517F20EB673E00448776 /* IRCClientConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCClientConfig.h; sourceTree = ""; }; 4C31518020EB673E00448776 /* IRCWorld.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCWorld.h; sourceTree = ""; }; 4C31518120EB673E00448776 /* IRCNetworkList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCNetworkList.h; sourceTree = ""; }; 4C31518220EB673E00448776 /* TDCSheetBase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCSheetBase.h; sourceTree = ""; }; 4C31518520EB673E00448776 /* IRCServerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCServerInternal.h; sourceTree = ""; }; 4C31518620EB673E00448776 /* IRCUserInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCUserInternal.h; sourceTree = ""; }; 4C31518720EB673E00448776 /* IRCAddressBookInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCAddressBookInternal.h; sourceTree = ""; }; 4C31518820EB673E00448776 /* TDCChannelPropertiesSheetInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelPropertiesSheetInternal.h; sourceTree = ""; }; 4C31518920EB673E00448776 /* TDCChannelSpotlightAppearanceInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelSpotlightAppearanceInternal.h; sourceTree = ""; }; 4C31518B20EB673E00448776 /* IRCPrefixInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCPrefixInternal.h; sourceTree = ""; }; 4C31518C20EB673E00448776 /* IRCChannelConfigInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelConfigInternal.h; sourceTree = ""; }; 4C31518D20EB673E00448776 /* IRCClientConfigInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCClientConfigInternal.h; sourceTree = ""; }; 4C31518E20EB673E00448776 /* TVCLogLineInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogLineInternal.h; sourceTree = ""; }; 4C31518F20EB673E00448776 /* IRCChannelUserInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelUserInternal.h; sourceTree = ""; }; 4C31519020EB673E00448776 /* IRCModeInfoInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCModeInfoInternal.h; sourceTree = ""; }; 4C31519120EB673E00448776 /* IRCMessageInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCMessageInternal.h; sourceTree = ""; }; 4C31519220EB673E00448776 /* TDCFileTransferDialogInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCFileTransferDialogInternal.h; sourceTree = ""; }; 4C31519320EB673E00448776 /* TDCChannelSpotlightControllerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelSpotlightControllerInternal.h; sourceTree = ""; }; 4C31519420EB673E00448776 /* IRCHighlightLogEntryInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCHighlightLogEntryInternal.h; sourceTree = ""; }; 4C31519520EB673E00448776 /* IRCHighlightMatchConditionInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCHighlightMatchConditionInternal.h; sourceTree = ""; }; 4C31519620EB673E00448776 /* TVCChannelSelectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCChannelSelectionViewController.h; sourceTree = ""; }; 4C31519720EB673E00448776 /* IRCTreeItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCTreeItem.h; sourceTree = ""; }; 4C31519820EB673E00448776 /* IRCISupportInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCISupportInfo.h; sourceTree = ""; }; 4C31519920EB673E00448776 /* TVCLogLine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogLine.h; sourceTree = ""; }; 4C31519A20EB673E00448776 /* IRCChannelMode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelMode.h; sourceTree = ""; }; 4C31519B20EB673E00448776 /* TXMenuController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXMenuController.h; sourceTree = ""; }; 4C31519C20EB673E00448776 /* TPCPreferencesReload.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPreferencesReload.h; sourceTree = ""; }; 4C31519D20EB673E00448776 /* TLOLinkParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOLinkParser.h; sourceTree = ""; }; 4C31519E20EB673E00448776 /* NSColorHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSColorHelper.h; sourceTree = ""; }; 4C31519F20EB673E00448776 /* TPCPreferencesUserDefaultsLocal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPreferencesUserDefaultsLocal.h; sourceTree = ""; }; 4C3151A020EB673E00448776 /* TVCErrorMessagePopoverPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCErrorMessagePopoverPrivate.h; sourceTree = ""; }; 4C3151A120EB673E00448776 /* IRCHighlightLogEntry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCHighlightLogEntry.h; sourceTree = ""; }; 4C3151A220EB673E00448776 /* TVCBasicTableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCBasicTableView.h; sourceTree = ""; }; 4C3151A320EB673E00448776 /* TVCAutoExpandingTokenField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCAutoExpandingTokenField.h; sourceTree = ""; }; 4C3151A420EB673E00448776 /* IRCChannelConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelConfig.h; sourceTree = ""; }; 4C3151A520EB673E00448776 /* IRCPrefix.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCPrefix.h; sourceTree = ""; }; 4C3151A620EB673E00448776 /* THOUnicodeHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = THOUnicodeHelper.h; sourceTree = ""; }; 4C3151A720EB673E00448776 /* IRCAddressBook.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCAddressBook.h; sourceTree = ""; }; 4C3151A820EB673E00448776 /* TLOKeyEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOKeyEventHandler.h; sourceTree = ""; }; 4C3151AA20EB673E00448776 /* TLONotificationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLONotificationController.h; sourceTree = ""; }; 4C3151AD20EB673E00448776 /* WebScriptObjectHelperPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebScriptObjectHelperPrivate.h; sourceTree = ""; }; 4C3151AE20EB673E00448776 /* IRCCommandIndexPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCCommandIndexPrivate.h; sourceTree = ""; }; 4C3151AF20EB673E00448776 /* TLONotificationControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLONotificationControllerPrivate.h; sourceTree = ""; }; 4C3151B020EB673E00448776 /* TVCLogScriptEventSinkPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogScriptEventSinkPrivate.h; sourceTree = ""; }; 4C3151B120EB673E00448776 /* IRCColorFormatPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCColorFormatPrivate.h; sourceTree = ""; }; 4C3151B220EB673E00448776 /* IRCHighlightLogEntryPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCHighlightLogEntryPrivate.h; sourceTree = ""; }; 4C3151B320EB673E00448776 /* THOPluginProtocolPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = THOPluginProtocolPrivate.h; sourceTree = ""; }; 4C3151B420EB673E00448776 /* IRCUserRelationsPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCUserRelationsPrivate.h; sourceTree = ""; }; 4C3151B520EB673E00448776 /* TDCChannelPropertiesNotificationConfigurationPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelPropertiesNotificationConfigurationPrivate.h; sourceTree = ""; }; 4C3151B620EB673E00448776 /* TVCMemberListUserInfoPopoverPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMemberListUserInfoPopoverPrivate.h; sourceTree = ""; }; 4C3151B820EB673E00448776 /* IRCTimerCommandPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCTimerCommandPrivate.h; sourceTree = ""; }; 4C3151B920EB673E00448776 /* TDCChannelBanListSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelBanListSheetPrivate.h; sourceTree = ""; }; 4C3151BA20EB673E00448776 /* TDCAddressBookSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCAddressBookSheetPrivate.h; sourceTree = ""; }; 4C3151BB20EB673E00448776 /* TVCMainWindowTextViewPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowTextViewPrivate.h; sourceTree = ""; }; 4C3151BC20EB673E00448776 /* IRCUserPersistentStorePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCUserPersistentStorePrivate.h; sourceTree = ""; }; 4C3151BD20EB673E00448776 /* TDCFileTransferDialogTableCellPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCFileTransferDialogTableCellPrivate.h; sourceTree = ""; }; 4C3151BE20EB673E00448776 /* IRCMessagePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCMessagePrivate.h; sourceTree = ""; }; 4C3151BF20EB673E00448776 /* TVCMemberListPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMemberListPrivate.h; sourceTree = ""; }; 4C3151C120EB673E00448776 /* IRCServerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCServerPrivate.h; sourceTree = ""; }; 4C3151C220EB673E00448776 /* TVCLogControllerInlineMediaServicePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogControllerInlineMediaServicePrivate.h; sourceTree = ""; }; 4C3151C320EB673E00448776 /* TDCServerChannelListDialogPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCServerChannelListDialogPrivate.h; sourceTree = ""; }; 4C3151C420EB673E00448776 /* TDCWelcomeSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCWelcomeSheetPrivate.h; sourceTree = ""; }; 4C3151C520EB673E00448776 /* TVCMemberListCellPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMemberListCellPrivate.h; sourceTree = ""; }; 4C3151C620EB673E00448776 /* TXSharedApplicationPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXSharedApplicationPrivate.h; sourceTree = ""; }; 4C3151C720EB673E00448776 /* IRCClientConfigPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCClientConfigPrivate.h; sourceTree = ""; }; 4C3151C820EB673E00448776 /* TVCMainWindowChannelViewPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowChannelViewPrivate.h; sourceTree = ""; }; 4C3151C920EB673E00448776 /* TVCLogControllerOperationQueuePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogControllerOperationQueuePrivate.h; sourceTree = ""; }; 4C3151CA20EB673E00448776 /* TVCServerListPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCServerListPrivate.h; sourceTree = ""; }; 4C3151CC20EB673E00448776 /* TDCLicenseUpgradeDialogPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCLicenseUpgradeDialogPrivate.h; sourceTree = ""; }; 4C3151CD20EB673E00448776 /* TVCWK1AutoScrollerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCWK1AutoScrollerPrivate.h; sourceTree = ""; }; 4C3151CE20EB673E00448776 /* TVCMainWindowSplitViewPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowSplitViewPrivate.h; sourceTree = ""; }; 4C3151CF20EB673E00448776 /* THOPluginManagerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = THOPluginManagerPrivate.h; sourceTree = ""; }; 4C3151D120EB673E00448776 /* TDCChannelSpotlightControlsPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelSpotlightControlsPrivate.h; sourceTree = ""; }; 4C3151D220EB673E00448776 /* TPCPathInfoPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPathInfoPrivate.h; sourceTree = ""; }; 4C3151D320EB673E00448776 /* TDCLicenseUpgradeCommonActionsPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCLicenseUpgradeCommonActionsPrivate.h; sourceTree = ""; }; 4C3151D520EB673E00448776 /* TVCAppearancePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCAppearancePrivate.h; sourceTree = ""; }; 4C3151D620EB673E00448776 /* TPCThemePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCThemePrivate.h; sourceTree = ""; }; 4C3151D720EB673E00448776 /* TDCHighlightEntrySheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCHighlightEntrySheetPrivate.h; sourceTree = ""; }; 4C3151D820EB673E00448776 /* TextualPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TextualPrivate.h; sourceTree = ""; }; 4C3151D920EB673E00448776 /* TDCChannelSpotlightSearchResultPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelSpotlightSearchResultPrivate.h; sourceTree = ""; }; 4C3151DA20EB673E00448776 /* TLOInputHistoryPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOInputHistoryPrivate.h; sourceTree = ""; }; 4C3151DB20EB673E00448776 /* IRCWorldPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCWorldPrivate.h; sourceTree = ""; }; 4C3151DC20EB673E00448776 /* TDCProgressIndicatorSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCProgressIndicatorSheetPrivate.h; sourceTree = ""; }; 4C3151DD20EB673E00448776 /* TVCLogPolicyPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogPolicyPrivate.h; sourceTree = ""; }; 4C3151DE20EB673E00448776 /* TXMenuControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXMenuControllerPrivate.h; sourceTree = ""; }; 4C3151DF20EB673E00448776 /* TVCMainWindowTitlebarAccessoryViewPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowTitlebarAccessoryViewPrivate.h; sourceTree = ""; }; 4C3151E020EB673E00448776 /* IRCAddressBookMatchCachePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCAddressBookMatchCachePrivate.h; sourceTree = ""; }; 4C3151E120EB673E00448776 /* IRCMessageBatchPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCMessageBatchPrivate.h; sourceTree = ""; }; 4C3151E220EB673E00448776 /* NSTableVIewHelperPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSTableVIewHelperPrivate.h; sourceTree = ""; }; 4C3151E320EB673E00448776 /* IRCTreeItemPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCTreeItemPrivate.h; sourceTree = ""; }; 4C3151E420EB673E00448776 /* TPCPreferencesImportExportPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPreferencesImportExportPrivate.h; sourceTree = ""; }; 4C3151E620EB673E00448776 /* TDCNicknameColorSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCNicknameColorSheetPrivate.h; sourceTree = ""; }; 4C3151E720EB673E00448776 /* IRCUserPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCUserPrivate.h; sourceTree = ""; }; 4C3151E820EB673E00448776 /* TDCChannelModifyTopicSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelModifyTopicSheetPrivate.h; sourceTree = ""; }; 4C3151E920EB673E00448776 /* TDCServerPropertiesSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCServerPropertiesSheetPrivate.h; sourceTree = ""; }; 4C3151EA20EB673E00448776 /* IRCAddressBookUserTrackingPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCAddressBookUserTrackingPrivate.h; sourceTree = ""; }; 4C3151EB20EB673E00448776 /* TDCServerEndpointListSheetTablePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCServerEndpointListSheetTablePrivate.h; sourceTree = ""; }; 4C3151ED20EB673E00448776 /* IRCISupportInfoPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCISupportInfoPrivate.h; sourceTree = ""; }; 4C3151EE20EB673E00448776 /* TPCApplicationInfoPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCApplicationInfoPrivate.h; sourceTree = ""; }; 4C3151EF20EB673E00448776 /* TDCServerChangeNicknameSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCServerChangeNicknameSheetPrivate.h; sourceTree = ""; }; 4C3151F020EB673E00448776 /* TVCLogViewInternalWK2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogViewInternalWK2.h; sourceTree = ""; }; 4C3151F120EB673E00448776 /* TDCPreferencesControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCPreferencesControllerPrivate.h; sourceTree = ""; }; 4C3151F220EB673E00448776 /* TLOFileLoggerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOFileLoggerPrivate.h; sourceTree = ""; }; 4C3151F320EB673E00448776 /* TVCMainWindowTextViewAppearancePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowTextViewAppearancePrivate.h; sourceTree = ""; }; 4C3151F420EB673E00448776 /* TVCMainWindowLoadingScreenPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowLoadingScreenPrivate.h; sourceTree = ""; }; 4C3151F620EB673E00448776 /* IRCExtrasPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCExtrasPrivate.h; sourceTree = ""; }; 4C3151F720EB673E00448776 /* TVCServerListCellPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCServerListCellPrivate.h; sourceTree = ""; }; 4C3151F820EB673E00448776 /* TVCChannelSelectionOutlineViewCellPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCChannelSelectionOutlineViewCellPrivate.h; sourceTree = ""; }; 4C3151FB20EB673E00448776 /* TDCSharedProtocolDefinitionsPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCSharedProtocolDefinitionsPrivate.h; sourceTree = ""; }; 4C3151FC20EB673E00448776 /* TDCLicenseUpgradeEligibilitySheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCLicenseUpgradeEligibilitySheetPrivate.h; sourceTree = ""; }; 4C3151FD20EB673E00448776 /* TDCLicenseManagerMigrateAppStoreSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCLicenseManagerMigrateAppStoreSheetPrivate.h; sourceTree = ""; }; 4C3151FF20EB673E00448776 /* IRCUserNicknameColorStyleGeneratorPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCUserNicknameColorStyleGeneratorPrivate.h; sourceTree = ""; }; 4C31520020EB673E00448776 /* TVCMainWindowAppearancePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowAppearancePrivate.h; sourceTree = ""; }; 4C31520120EB673E00448776 /* TLOEncryptionManagerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOEncryptionManagerPrivate.h; sourceTree = ""; }; 4C31520220EB673E00448776 /* IRCChannelPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelPrivate.h; sourceTree = ""; }; 4C31520320EB673E00448776 /* TLOSpokenNotificationPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOSpokenNotificationPrivate.h; sourceTree = ""; }; 4C31520420EB673E00448776 /* TXMasterControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXMasterControllerPrivate.h; sourceTree = ""; }; 4C31520520EB673E00448776 /* TVCChannelSelectionViewControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCChannelSelectionViewControllerPrivate.h; sourceTree = ""; }; 4C31520620EB673E00448776 /* TXApplicationPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXApplicationPrivate.h; sourceTree = ""; }; 4C31520720EB673E00448776 /* TDCFileTransferDialogTransferControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCFileTransferDialogTransferControllerPrivate.h; sourceTree = ""; }; 4C31520820EB673E00448776 /* TVCTextViewWithIRCFormatterPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCTextViewWithIRCFormatterPrivate.h; sourceTree = ""; }; 4C31520920EB673E00448776 /* TLOLicenseManagerDownloaderPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOLicenseManagerDownloaderPrivate.h; sourceTree = ""; }; 4C31520A20EB673E00448776 /* SwiftBridgingHeaderPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgingHeaderPrivate.h; sourceTree = ""; }; 4C31520B20EB673E00448776 /* IRCClientPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCClientPrivate.h; sourceTree = ""; }; 4C31520D20EB673E00448776 /* TVCLogViewPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogViewPrivate.h; sourceTree = ""; }; 4C31520E20EB673E00448776 /* TDCLicenseUpgradeActivateSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCLicenseUpgradeActivateSheetPrivate.h; sourceTree = ""; }; 4C31520F20EB673E00448776 /* TDCServerHighlightListSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCServerHighlightListSheetPrivate.h; sourceTree = ""; }; 4C31521020EB673E00448776 /* TPCThemeControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCThemeControllerPrivate.h; sourceTree = ""; }; 4C31521120EB673E00448776 /* IRCChannelUserPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelUserPrivate.h; sourceTree = ""; }; 4C31521220EB673E00448776 /* TDCFileTransferDialogPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCFileTransferDialogPrivate.h; sourceTree = ""; }; 4C31521420EB673E00448776 /* NSViewHelperPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSViewHelperPrivate.h; sourceTree = ""; }; 4C31521520EB673E00448776 /* TVCLogControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogControllerPrivate.h; sourceTree = ""; }; 4C31521620EB673E00448776 /* THOPluginDispatcherPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = THOPluginDispatcherPrivate.h; sourceTree = ""; }; 4C31521720EB673E00448776 /* TVCServerListAppearancePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCServerListAppearancePrivate.h; sourceTree = ""; }; 4C31521820EB673E00448776 /* TPCPreferencesLocalPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPreferencesLocalPrivate.h; sourceTree = ""; }; 4C31521920EB673E00448776 /* THOPluginItemPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = THOPluginItemPrivate.h; sourceTree = ""; }; 4C31521A20EB673E00448776 /* TVCMemberListAppearancePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMemberListAppearancePrivate.h; sourceTree = ""; }; 4C31521B20EB673E00448776 /* TDCChannelSpotlightAppearancePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelSpotlightAppearancePrivate.h; sourceTree = ""; }; 4C31521D20EB673E00448776 /* TVCLogViewInternalWK1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogViewInternalWK1.h; sourceTree = ""; }; 4C31521F20EB673E00448776 /* TVCContentNavigationOutlineViewPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCContentNavigationOutlineViewPrivate.h; sourceTree = ""; }; 4C31522020EB673E00448776 /* IRCConnectionPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCConnectionPrivate.h; sourceTree = ""; }; 4C31522120EB673E00448776 /* IRCChannelConfigPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelConfigPrivate.h; sourceTree = ""; }; 4C31522220EB673E00448776 /* TLOLicenseManagerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOLicenseManagerPrivate.h; sourceTree = ""; }; 4C31522320EB673E00448776 /* TVCNotificationConfigurationViewControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCNotificationConfigurationViewControllerPrivate.h; sourceTree = ""; }; 4C31522420EB673E00448776 /* TVCLogControllerHistoricLogFilePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogControllerHistoricLogFilePrivate.h; sourceTree = ""; }; 4C31522520EB673E00448776 /* TPCResourceManagerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCResourceManagerPrivate.h; sourceTree = ""; }; 4C31522620EB673E00448776 /* TVCTextFormatterMenuPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCTextFormatterMenuPrivate.h; sourceTree = ""; }; 4C31522720EB673E00448776 /* TVCDockIconPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCDockIconPrivate.h; sourceTree = ""; }; 4C31522820EB673E00448776 /* TDCAboutDialogPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCAboutDialogPrivate.h; sourceTree = ""; }; 4C31522920EB673E00448776 /* IRCChannelModePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannelModePrivate.h; sourceTree = ""; }; 4C31522A20EB673E00448776 /* TXWindowControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXWindowControllerPrivate.h; sourceTree = ""; }; 4C31522B20EB673E00448776 /* TDCChannelSpotlightControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelSpotlightControllerPrivate.h; sourceTree = ""; }; 4C31522C20EB673E00448776 /* TDCChannelSpotlightSearchResultsTablePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelSpotlightSearchResultsTablePrivate.h; sourceTree = ""; }; 4C31522D20EB673E00448776 /* TVCMainWindowSegmentedControlPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowSegmentedControlPrivate.h; sourceTree = ""; }; 4C31522F20EB673E00448776 /* TDCServerEndpointListSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCServerEndpointListSheetPrivate.h; sourceTree = ""; }; 4C31523020EB673E00448776 /* TVCLogLinePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogLinePrivate.h; sourceTree = ""; }; 4C31523120EB673E00448776 /* WKWebViewPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKWebViewPrivate.h; sourceTree = ""; }; 4C31523220EB673E00448776 /* TXGlobalModelsPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXGlobalModelsPrivate.h; sourceTree = ""; }; 4C31523320EB673E00448776 /* TLOLicenseManagerLastGenPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOLicenseManagerLastGenPrivate.h; sourceTree = ""; }; 4C31523420EB673E00448776 /* TLONotificationConfigurationPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLONotificationConfigurationPrivate.h; sourceTree = ""; }; 4C31523520EB673E00448776 /* TLONicknameCompletionStatusPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLONicknameCompletionStatusPrivate.h; sourceTree = ""; }; 4C31523620EB673E00448776 /* TLOSpeechSynthesizerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOSpeechSynthesizerPrivate.h; sourceTree = ""; }; 4C31523720EB673E00448776 /* TDCLicenseManagerDialogPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCLicenseManagerDialogPrivate.h; sourceTree = ""; }; 4C31523820EB673E00448776 /* TVCMainWindowPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowPrivate.h; sourceTree = ""; }; 4C31523920EB673E00448776 /* TDCPreferencesNotificationConfigurationPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCPreferencesNotificationConfigurationPrivate.h; sourceTree = ""; }; 4C31523A20EB673E00448776 /* TXAppearancePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXAppearancePrivate.h; sourceTree = ""; }; 4C31523B20EB673E00448776 /* TDCChannelPropertiesSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelPropertiesSheetPrivate.h; sourceTree = ""; }; 4C31523C20EB673E00448776 /* TDCLicenseManagerRecoverLostLicenseSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCLicenseManagerRecoverLostLicenseSheetPrivate.h; sourceTree = ""; }; 4C31523D20EB673E00448776 /* TDCChannelModifyModesSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelModifyModesSheetPrivate.h; sourceTree = ""; }; 4C31523E20EB673E00448776 /* TDCChannelInviteSheetPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCChannelInviteSheetPrivate.h; sourceTree = ""; }; 4C31523F20EB673E00448776 /* IRCClientRequestedCommandsPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCClientRequestedCommandsPrivate.h; sourceTree = ""; }; 4C31524020EB673E00448776 /* TPCApplicationInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCApplicationInfo.h; sourceTree = ""; }; 4C31524120EB673E00448776 /* IRC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRC.h; sourceTree = ""; }; 4C31524220EB673E00448776 /* TVCMainWindowTextViewAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowTextViewAppearance.h; sourceTree = ""; }; 4C31524320EB673E00448776 /* NSViewHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSViewHelper.h; sourceTree = ""; }; 4C31524420EB673E00448776 /* TVCTextViewWithIRCFormatter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCTextViewWithIRCFormatter.h; sourceTree = ""; }; 4C31524520EB673E00448776 /* TLOEncryptionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOEncryptionManager.h; sourceTree = ""; }; 4C31524620EB673E00448776 /* IRCServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCServer.h; sourceTree = ""; }; 4C31524720EB673E00448776 /* IRCHighlightMatchCondition.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCHighlightMatchCondition.h; sourceTree = ""; }; 4C31524820EB673E00448776 /* TXGlobalModels.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TXGlobalModels.h; sourceTree = ""; }; 4C31524920EB673E00448776 /* TDCAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCAlert.h; sourceTree = ""; }; 4C31524A20EB673E00448776 /* TPCPreferencesImportExport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPreferencesImportExport.h; sourceTree = ""; }; 4C31524B20EB673E00448776 /* IRCChannel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCChannel.h; sourceTree = ""; }; 4C31524C20EB673E00448776 /* TVCMainWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindow.h; sourceTree = ""; }; 4C31524D20EB673E00448776 /* TVCLogView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogView.h; sourceTree = ""; }; 4C31524E20EB673E00448776 /* IRCUser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCUser.h; sourceTree = ""; }; 4C31524F20EB673E00448776 /* IRCUserRelations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCUserRelations.h; sourceTree = ""; }; 4C31525020EB673E00448776 /* TVCValidatedComboBox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCValidatedComboBox.h; sourceTree = ""; }; 4C31525120EB673E00448776 /* TVCMemberListAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMemberListAppearance.h; sourceTree = ""; }; 4C31525220EB673E00448776 /* TVCMainWindowSplitView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowSplitView.h; sourceTree = ""; }; 4C31525320EB673E00448776 /* IRCMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCMessage.h; sourceTree = ""; }; 4C31525420EB673E00448776 /* TVCLogRenderer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogRenderer.h; sourceTree = ""; }; 4C31525520EB673E00448776 /* IRCSendingMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCSendingMessage.h; sourceTree = ""; }; 4C31525620EB673E00448776 /* TVCLogController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCLogController.h; sourceTree = ""; }; 4C31525720EB673E00448776 /* TVCMainWindowLoadingScreen.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowLoadingScreen.h; sourceTree = ""; }; 4C31525820EB673E00448776 /* NSStringHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSStringHelper.h; sourceTree = ""; }; 4C31525920EB673E00448776 /* TVCMainWindowTextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowTextView.h; sourceTree = ""; }; 4C31525A20EB673E00448776 /* TVCMainWindowAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCMainWindowAppearance.h; sourceTree = ""; }; 4C31525B20EB673E00448776 /* TLOInternetAddressLookup.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOInternetAddressLookup.h; sourceTree = ""; }; 4C31525C20EB673E00448776 /* TDCWindowBase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TDCWindowBase.h; sourceTree = ""; }; 4C31525E20EB673E00448776 /* TVCValidatedTextField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVCValidatedTextField.h; sourceTree = ""; }; 4C31525F20EB673E00448776 /* Textual.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Textual.h; sourceTree = ""; }; 4C31526020EB673E00448776 /* IRCConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCConnection.h; sourceTree = ""; }; 4C31526120EB673E00448776 /* IRCColorFormat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCColorFormat.h; sourceTree = ""; }; 4C31526320EB673E00448776 /* TDCChannelInviteSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelInviteSheet.m; sourceTree = ""; }; 4C31526420EB673E00448776 /* TDCChannelPropertiesSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelPropertiesSheet.m; sourceTree = ""; }; 4C31526520EB673E00448776 /* TDCServerPropertiesSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCServerPropertiesSheet.m; sourceTree = ""; }; 4C31526620EB673E00448776 /* TDCChannelModifyModesSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelModifyModesSheet.m; sourceTree = ""; }; 4C31526820EB673E00448776 /* TDCPreferencesNotificationConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCPreferencesNotificationConfiguration.m; sourceTree = ""; }; 4C31526920EB673E00448776 /* TDCPreferencesController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCPreferencesController.m; sourceTree = ""; }; 4C31526B20EB673E00448776 /* TDCChannelSpotlightControls.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelSpotlightControls.m; sourceTree = ""; }; 4C31526C20EB673E00448776 /* TDCChannelSpotlightSearchResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelSpotlightSearchResult.m; sourceTree = ""; }; 4C31526D20EB673E00448776 /* TDCChannelSpotlightController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelSpotlightController.m; sourceTree = ""; }; 4C31526E20EB673E00448776 /* TDCChannelSpotlightAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelSpotlightAppearance.m; sourceTree = ""; }; 4C31526F20EB673E00448776 /* TDCChannelSpotlightSearchResultsTable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelSpotlightSearchResultsTable.m; sourceTree = ""; }; 4C31527020EB673E00448776 /* TDCAddressBookSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCAddressBookSheet.m; sourceTree = ""; }; 4C31527120EB673E00448776 /* TDCChannelPropertiesNotificationConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelPropertiesNotificationConfiguration.m; sourceTree = ""; }; 4C31527420EB673E00448776 /* TDCLicenseUpgradeDialog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCLicenseUpgradeDialog.m; sourceTree = ""; }; 4C31527520EB673E00448776 /* TDCLicenseUpgradeEligibilitySheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCLicenseUpgradeEligibilitySheet.m; sourceTree = ""; }; 4C31527620EB673E00448776 /* TDCLicenseManagerMigrateAppStoreSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCLicenseManagerMigrateAppStoreSheet.m; sourceTree = ""; }; 4C31527720EB673E00448776 /* TDCLicenseUpgradeCommonActions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCLicenseUpgradeCommonActions.m; sourceTree = ""; }; 4C31527820EB673E00448776 /* TDCLicenseManagerRecoverLostLicenseSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCLicenseManagerRecoverLostLicenseSheet.m; sourceTree = ""; }; 4C31527920EB673E00448776 /* TDCLicenseUpgradeActivateSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCLicenseUpgradeActivateSheet.m; sourceTree = ""; }; 4C31527A20EB673E00448776 /* TDCLicenseManagerDialog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCLicenseManagerDialog.m; sourceTree = ""; }; 4C31528020EB673E00448776 /* TDCAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCAlert.m; sourceTree = ""; }; 4C31528120EB673E00448776 /* TDCAboutDialog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCAboutDialog.m; sourceTree = ""; }; 4C31528220EB673E00448776 /* TDCChannelBanListSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelBanListSheet.m; sourceTree = ""; }; 4C31528320EB673E00448776 /* TDCHighlightEntrySheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCHighlightEntrySheet.m; sourceTree = ""; }; 4C31528420EB673E00448776 /* TDCWindowBase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCWindowBase.m; sourceTree = ""; }; 4C31528520EB673E00448776 /* TDCInputPrompt.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCInputPrompt.m; sourceTree = ""; }; 4C31528620EB673E00448776 /* TDCServerChannelListDialog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCServerChannelListDialog.m; sourceTree = ""; }; 4C31528720EB673E00448776 /* TDCServerHighlightListSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCServerHighlightListSheet.m; sourceTree = ""; }; 4C31528820EB673E00448776 /* TDCProgressIndicatorSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCProgressIndicatorSheet.m; sourceTree = ""; }; 4C31528920EB673E00448776 /* TDCChannelModifyTopicSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCChannelModifyTopicSheet.m; sourceTree = ""; }; 4C31528A20EB673E00448776 /* TDCSheetBase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCSheetBase.m; sourceTree = ""; }; 4C31528B20EB673E00448776 /* TDCWelcomeSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCWelcomeSheet.m; sourceTree = ""; }; 4C31528C20EB673E00448776 /* TDCServerChangeNicknameSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCServerChangeNicknameSheet.m; sourceTree = ""; }; 4C31528E20EB673E00448776 /* TDCServerEndpointListSheetTable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCServerEndpointListSheetTable.m; sourceTree = ""; }; 4C31528F20EB673E00448776 /* TDCServerEndpointListSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCServerEndpointListSheet.m; sourceTree = ""; }; 4C31529120EB673E00448776 /* TDCFileTransferDialog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCFileTransferDialog.m; sourceTree = ""; }; 4C31529220EB673E00448776 /* TDCFileTransferDialogTransferController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCFileTransferDialogTransferController.m; sourceTree = ""; }; 4C31529320EB673E00448776 /* TDCFileTransferDialogTableCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCFileTransferDialogTableCell.m; sourceTree = ""; }; 4C31529420EB673E00448776 /* TDCNicknameColorSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDCNicknameColorSheet.m; sourceTree = ""; }; 4C31529B20EB673E00448776 /* TXGlobalModels.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TXGlobalModels.m; sourceTree = ""; }; 4C31529C20EB673E00448776 /* TXApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TXApplication.m; sourceTree = ""; }; 4C31529D20EB673E00448776 /* TXSharedApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TXSharedApplication.m; sourceTree = ""; }; 4C31529E20EB673E00448776 /* TXAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TXAppearance.m; sourceTree = ""; }; 4C31529F20EB673E00448776 /* TXMasterController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TXMasterController.m; sourceTree = ""; }; 4C3152A020EB673E00448776 /* TXWindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TXWindowController.m; sourceTree = ""; }; 4C3152A120EB673E00448776 /* TXMenuController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TXMenuController.m; sourceTree = ""; }; 4C3152A320EB673E00448776 /* TVCBasicTableView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCBasicTableView.m; sourceTree = ""; }; 4C3152A520EB673E00448776 /* TVCAutoExpandingTokenField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCAutoExpandingTokenField.m; sourceTree = ""; }; 4C3152A620EB673E00448776 /* TVCTextFormatterMenu.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCTextFormatterMenu.m; sourceTree = ""; }; 4C3152A720EB673E00448776 /* TVCTextViewWithIRCFormatter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCTextViewWithIRCFormatter.m; sourceTree = ""; }; 4C3152A820EB673E00448776 /* TVCValidatedComboBox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCValidatedComboBox.m; sourceTree = ""; }; 4C3152A920EB673E00448776 /* TVCValidatedTextField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCValidatedTextField.m; sourceTree = ""; }; 4C3152AA20EB673E00448776 /* TVCAutoExpandingTextField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCAutoExpandingTextField.m; sourceTree = ""; }; 4C3152AC20EB673E00448776 /* TVCNotificationConfigurationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCNotificationConfigurationViewController.m; sourceTree = ""; }; 4C3152AE20EB673E00448776 /* TVCChannelSelectionOutlineCellView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCChannelSelectionOutlineCellView.m; sourceTree = ""; }; 4C3152AF20EB673E00448776 /* TVCChannelSelectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCChannelSelectionViewController.m; sourceTree = ""; }; 4C3152B120EB673E00448776 /* TVCMemberListCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMemberListCell.m; sourceTree = ""; }; 4C3152B220EB673E00448776 /* TVCMemberListAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMemberListAppearance.m; sourceTree = ""; }; 4C3152B320EB673E00448776 /* TVCMemberList.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMemberList.m; sourceTree = ""; }; 4C3152B420EB673E00448776 /* TVCMemberListUserInfoPopover.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMemberListUserInfoPopover.m; sourceTree = ""; }; 4C3152B520EB673E00448776 /* TVCContentNavigationOutlineView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCContentNavigationOutlineView.m; sourceTree = ""; }; 4C3152B720EB673E00448776 /* TVCLogScriptEventSink.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogScriptEventSink.m; sourceTree = ""; }; 4C3152B820EB673E00448776 /* TVCLogPolicy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogPolicy.m; sourceTree = ""; }; 4C3152B920EB673E00448776 /* TVCLogViewInternalWK2.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogViewInternalWK2.m; sourceTree = ""; }; 4C3152BA20EB673E00448776 /* TVCLogController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogController.m; sourceTree = ""; }; 4C3152BB20EB673E00448776 /* TVCLogRenderer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogRenderer.m; sourceTree = ""; }; 4C3152BC20EB673E00448776 /* TVCLogViewInternalWK1.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogViewInternalWK1.m; sourceTree = ""; }; 4C3152BD20EB673E00448776 /* TVCLogView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogView.m; sourceTree = ""; }; 4C3152BE20EB673E00448776 /* TVCLogLine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogLine.m; sourceTree = ""; }; 4C3152BF20EB673E00448776 /* TVCWK1AutoScroller.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCWK1AutoScroller.m; sourceTree = ""; }; 4C3152C120EB673E00448776 /* TVCLogControllerInlineMediaService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogControllerInlineMediaService.m; sourceTree = ""; }; 4C3152C220EB673E00448776 /* TVCLogControllerHistoricLogFile.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogControllerHistoricLogFile.m; sourceTree = ""; }; 4C3152C320EB673E00448776 /* TVCLogControllerOperationQueue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCLogControllerOperationQueue.m; sourceTree = ""; }; 4C3152C420EB673E00448776 /* TVCAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCAppearance.m; sourceTree = ""; }; 4C3152C520EB673E00448776 /* TVCDockIcon.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCDockIcon.m; sourceTree = ""; }; 4C3152C720EB673E00448776 /* TVCErrorMessagePopover.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCErrorMessagePopover.m; sourceTree = ""; }; 4C3152C820EB673E00448776 /* TVCErrorMessagePopoverController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCErrorMessagePopoverController.m; sourceTree = ""; }; 4C3152C920EB673E00448776 /* TVCAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCAlert.m; sourceTree = ""; }; 4C3152CB20EB673E00448776 /* TVCServerListCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCServerListCell.m; sourceTree = ""; }; 4C3152CC20EB673E00448776 /* TVCServerList.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCServerList.m; sourceTree = ""; }; 4C3152CD20EB673E00448776 /* TVCServerListAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCServerListAppearance.m; sourceTree = ""; }; 4C3152CF20EB673E00448776 /* TVCMainWindowTextViewAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindowTextViewAppearance.m; sourceTree = ""; }; 4C3152D120EB673E00448776 /* TVCMainWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindow.m; sourceTree = ""; }; 4C3152D220EB673E00448776 /* TVCMainWindowLoadingScreen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindowLoadingScreen.m; sourceTree = ""; }; 4C3152D320EB673E00448776 /* TVCMainWindowSplitView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindowSplitView.m; sourceTree = ""; }; 4C3152D420EB673E00448776 /* TVCMainWindowSegmentedControl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindowSegmentedControl.m; sourceTree = ""; }; 4C3152D520EB673E00448776 /* TVCMainWindowAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindowAppearance.m; sourceTree = ""; }; 4C3152D620EB673E00448776 /* TVCMainWindowTextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindowTextView.m; sourceTree = ""; }; 4C3152D720EB673E00448776 /* TVCMainWindowTitlebarAccessoryView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindowTitlebarAccessoryView.m; sourceTree = ""; }; 4C3152D820EB673E00448776 /* TVCMainWindowChannelView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVCMainWindowChannelView.m; sourceTree = ""; }; 4C3152DC20EB673E00448776 /* WebScriptObjectHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WebScriptObjectHelper.m; sourceTree = ""; }; 4C3152DE20EB673E00448776 /* GTMEncodeHTML.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMEncodeHTML.m; sourceTree = ""; }; 4C3152DF20EB673E00448776 /* THOUnicodeHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = THOUnicodeHelper.m; sourceTree = ""; }; 4C3152E120EB673E00448776 /* THOPluginDispatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = THOPluginDispatcher.m; sourceTree = ""; }; 4C3152E220EB673E00448776 /* THOPluginManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = THOPluginManager.m; sourceTree = ""; }; 4C3152E320EB673E00448776 /* THOPluginItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = THOPluginItem.m; sourceTree = ""; }; 4C3152E520EB673E00448776 /* NSColorHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSColorHelper.m; sourceTree = ""; }; 4C3152E620EB673E00448776 /* NSViewHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSViewHelper.m; sourceTree = ""; }; 4C3152E720EB673E00448776 /* NSStringHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSStringHelper.m; sourceTree = ""; }; 4C3152E820EB673E00448776 /* NSTableViewHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSTableViewHelper.m; sourceTree = ""; }; 4C3152E920EB673E00448776 /* TXAppearanceHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TXAppearanceHelper.m; sourceTree = ""; }; 4C3152EE20EB676400448776 /* RemoteLicenseSystemPublicKey.pub */ = {isa = PBXFileReference; lastKnownFileType = text; path = RemoteLicenseSystemPublicKey.pub; sourceTree = ""; }; 4C31531920EB676400448776 /* TPWTB_Advanced@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "TPWTB_Advanced@2x.png"; sourceTree = ""; }; 4C31531A20EB676400448776 /* TPWTB_Highlights.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TPWTB_Highlights.png; sourceTree = ""; }; 4C31531B20EB676400448776 /* TPWTB_Highlights@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "TPWTB_Highlights@2x.png"; sourceTree = ""; }; 4C31531C20EB676400448776 /* TPWTB_Addons.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TPWTB_Addons.png; sourceTree = ""; }; 4C31531D20EB676400448776 /* TPWTB_Advanced.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TPWTB_Advanced.png; sourceTree = ""; }; 4C31531E20EB676400448776 /* TPWTB_Controls.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TPWTB_Controls.png; sourceTree = ""; }; 4C31531F20EB676400448776 /* TPWTB_Style@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "TPWTB_Style@2x.png"; sourceTree = ""; }; 4C31532020EB676400448776 /* TPWTB_General@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "TPWTB_General@2x.png"; sourceTree = ""; }; 4C31532120EB676400448776 /* TPWTB_Addons@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "TPWTB_Addons@2x.png"; sourceTree = ""; }; 4C31532220EB676400448776 /* TPWTB_Notifications@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "TPWTB_Notifications@2x.png"; sourceTree = ""; }; 4C31532320EB676400448776 /* TPWTB_Controls@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "TPWTB_Controls@2x.png"; sourceTree = ""; }; 4C31532420EB676400448776 /* TPWTB_Interface.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TPWTB_Interface.png; sourceTree = ""; }; 4C31532520EB676400448776 /* TPWTB_Interface@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "TPWTB_Interface@2x.png"; sourceTree = ""; }; 4C31532620EB676400448776 /* TPWTB_Notifications.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TPWTB_Notifications.png; sourceTree = ""; }; 4C31532720EB676400448776 /* TPWTB_Style.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TPWTB_Style.png; sourceTree = ""; }; 4C31532820EB676400448776 /* TPWTB_General.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TPWTB_General.png; sourceTree = ""; }; 4C31533120EB676400448776 /* channelRoomStatusIconLightActive@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "channelRoomStatusIconLightActive@2x.tif"; sourceTree = ""; }; 4C31533220EB676400448776 /* channelRoomStatusIconDarkActive@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "channelRoomStatusIconDarkActive@2x.tif"; sourceTree = ""; }; 4C31533320EB676400448776 /* channelRoomStatusIconLightInactive.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = channelRoomStatusIconLightInactive.tif; sourceTree = ""; }; 4C31533420EB676400448776 /* channelRoomStatusIconDarkInactive@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "channelRoomStatusIconDarkInactive@2x.tif"; sourceTree = ""; }; 4C31533620EB676400448776 /* channelRoomStatusIconDarkActive.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = channelRoomStatusIconDarkActive.tif; sourceTree = ""; }; 4C31533720EB676400448776 /* channelRoomStatusIconLightActive.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = channelRoomStatusIconLightActive.tif; sourceTree = ""; }; 4C31533920EB676400448776 /* channelRoomStatusIconDarkInactive.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = channelRoomStatusIconDarkInactive.tif; sourceTree = ""; }; 4C31533B20EB676400448776 /* channelRoomStatusIconLightInactive@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "channelRoomStatusIconLightInactive@2x.tif"; sourceTree = ""; }; 4C31533D20EB676400448776 /* FormattingColor_8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_8.png; sourceTree = ""; }; 4C31533E20EB676400448776 /* FormattingColor_9.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_9.png; sourceTree = ""; }; 4C31533F20EB676400448776 /* FormattingColor_11.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_11.png; sourceTree = ""; }; 4C31534020EB676400448776 /* FormattingColor_10.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_10.png; sourceTree = ""; }; 4C31534120EB676400448776 /* FormattingColor_12.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_12.png; sourceTree = ""; }; 4C31534220EB676400448776 /* FormattingColor_Rainbow.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = FormattingColor_Rainbow.tif; sourceTree = ""; }; 4C31534320EB676400448776 /* FormattingColor_13.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_13.png; sourceTree = ""; }; 4C31534420EB676400448776 /* FormattingColor_14.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_14.png; sourceTree = ""; }; 4C31534520EB676400448776 /* FormattingColor_15.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_15.png; sourceTree = ""; }; 4C31534620EB676400448776 /* FormattingColor_7.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_7.png; sourceTree = ""; }; 4C31534720EB676400448776 /* FormattingColor_6.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_6.png; sourceTree = ""; }; 4C31534820EB676400448776 /* FormattingColor_4.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_4.png; sourceTree = ""; }; 4C31534920EB676400448776 /* FormattingColor_5.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_5.png; sourceTree = ""; }; 4C31534A20EB676400448776 /* FormattingColor_1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_1.png; sourceTree = ""; }; 4C31534B20EB676400448776 /* FormattingColor_Rainbow@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "FormattingColor_Rainbow@2x.tif"; sourceTree = ""; }; 4C31534C20EB676400448776 /* FormattingColor_0.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_0.png; sourceTree = ""; }; 4C31534D20EB676400448776 /* FormattingColor_2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_2.png; sourceTree = ""; }; 4C31534E20EB676400448776 /* FormattingColor_3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FormattingColor_3.png; sourceTree = ""; }; 4C31535020EB676400448776 /* encryptionLockIconLight@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "encryptionLockIconLight@2x.tif"; sourceTree = ""; }; 4C31535120EB676400448776 /* encryptionLockIconDark.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = encryptionLockIconDark.tif; sourceTree = ""; }; 4C31535220EB676400448776 /* encryptionLockIconDark@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "encryptionLockIconDark@2x.tif"; sourceTree = ""; }; 4C31535320EB676400448776 /* encryptionLockIconLight.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = encryptionLockIconLight.tif; sourceTree = ""; }; 4C31535620EB676400448776 /* Copyright Information for Images.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Copyright Information for Images.txt"; sourceTree = ""; }; 4C31535820EB676400448776 /* applicationIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = applicationIcon.icns; sourceTree = ""; }; 4C31535920EB676400448776 /* applicationIcon.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; path = applicationIcon.iconset; sourceTree = ""; }; 4C31535A20EB676400448776 /* applicationIconBirthday.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = applicationIconBirthday.icns; sourceTree = ""; }; 4C31535F20EB676400448776 /* DIGreenBadgeLeft.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = DIGreenBadgeLeft.png; sourceTree = ""; }; 4C31536020EB676400448776 /* DIGreenBadgeRight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = DIGreenBadgeRight.png; sourceTree = ""; }; 4C31536120EB676400448776 /* DIGreenBadgeCenter.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = DIGreenBadgeCenter.png; sourceTree = ""; }; 4C31537020EB676400448776 /* DIRedBadgeRight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = DIRedBadgeRight.png; sourceTree = ""; }; 4C31537120EB676400448776 /* DIRedBadgeLeft.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = DIRedBadgeLeft.png; sourceTree = ""; }; 4C31537220EB676400448776 /* DIRedBadgeCenter.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = DIRedBadgeCenter.png; sourceTree = ""; }; 4C31537820EB676400448776 /* MainWindowSegmentedControlUserTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = MainWindowSegmentedControlUserTemplate.pdf; sourceTree = ""; }; 4C31537920EB676400448776 /* MainWindowSpotlightIconTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = MainWindowSpotlightIconTemplate.pdf; sourceTree = ""; }; 4C31537B20EB676400448776 /* ErroneousTextFieldValueIndicator.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = ErroneousTextFieldValueIndicator.tif; sourceTree = ""; }; 4C31537E20EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif"; sourceTree = ""; }; 4C31537F20EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif"; sourceTree = ""; }; 4C31538220EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconActive.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = VibrantDarkServerListViewPrivateMessageUserIconActive.tif; sourceTree = ""; }; 4C31538320EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconActive.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = VibrantLightServerListViewPrivateMessageUserIconActive.tif; sourceTree = ""; }; 4C31538420EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconInactive.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = VibrantLightServerListViewPrivateMessageUserIconInactive.tif; sourceTree = ""; }; 4C31538520EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif"; sourceTree = ""; }; 4C31538620EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconInactive.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = VibrantDarkServerListViewPrivateMessageUserIconInactive.tif; sourceTree = ""; }; 4C31538720EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif"; sourceTree = ""; }; 4C31539F20EB676400448776 /* IRCCommandIndexRemoteData.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = IRCCommandIndexRemoteData.plist; sourceTree = ""; }; 4C3153A020EB676400448776 /* IRCCommandIndexLocalData.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = IRCCommandIndexLocalData.plist; sourceTree = ""; }; 4C3153A120EB676400448776 /* TemplateLineTypes.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = TemplateLineTypes.plist; sourceTree = ""; }; 4C3153A220EB676400448776 /* IRCNetworks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = IRCNetworks.plist; sourceTree = ""; }; 4C3153A420EB676400448776 /* StaticStore.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = StaticStore.plist; sourceTree = ""; }; 4C3153A520EB676400448776 /* RegisteredUserDefaults.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = RegisteredUserDefaults.plist; sourceTree = ""; }; 4C3153A620EB676400448776 /* RegisteredUserDefaultsInContainer.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = RegisteredUserDefaultsInContainer.plist; sourceTree = ""; }; 4C3154B720EB685700448776 /* Textual App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Textual App.xcconfig"; sourceTree = ""; }; 4C3154B820EB685700448776 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C3154B920EB685700448776 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C3154BA20EB685700448776 /* XPC Service - ICL.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL.xcconfig"; sourceTree = ""; }; 4C3154BB20EB685700448776 /* XPC Services.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4C3154BC20EB685700448776 /* XPC Service - ICL Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL Extensions.xcconfig"; sourceTree = ""; }; 4C3154BD20EB685700448776 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C3154CA20EB685700448776 /* Textual App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Textual App.xcconfig"; sourceTree = ""; }; 4C3154CB20EB685700448776 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C3154CC20EB685700448776 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4C3154CD20EB685700448776 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C3154CE20EB685700448776 /* XPC Service - ICL.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL.xcconfig"; sourceTree = ""; }; 4C3154CF20EB685700448776 /* Foundation.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4C3154D020EB685700448776 /* XPC Services.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4C3154D120EB685700448776 /* XPC Service - ICL Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL Extensions.xcconfig"; sourceTree = ""; }; 4C3154D220EB685700448776 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4C3154D320EB685700448776 /* Code Signing Identity.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Code Signing Identity.xcconfig"; sourceTree = ""; }; 4C3154DF20EB685700448776 /* Textual App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Textual App.xcconfig"; sourceTree = ""; }; 4C3154E020EB685700448776 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C3154E120EB685700448776 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C3154E220EB685700448776 /* XPC Service - ICL.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL.xcconfig"; sourceTree = ""; }; 4C3154E320EB685700448776 /* XPC Services.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4C3154E420EB685700448776 /* XPC Service - ICL Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL Extensions.xcconfig"; sourceTree = ""; }; 4C3154E520EB685700448776 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C3154E720EB68A900448776 /* Standard Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Standard Release.entitlements"; sourceTree = ""; }; 4C3154E820EB68A900448776 /* Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Debug.entitlements; sourceTree = ""; }; 4C315EE72C36581A001A16B4 /* KeysExcludedFromExport.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = KeysExcludedFromExport.plist; sourceTree = ""; }; 4C315EE82C36581A001A16B4 /* PreferenceKeyMasterList.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PreferenceKeyMasterList.plist; sourceTree = ""; }; 4C33341B20CC471A00ACB9AD /* Bundled Scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Bundled Scripts"; sourceTree = ""; }; 4C33342020CC475300ACB9AD /* Bundled Styles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Bundled Styles"; sourceTree = ""; }; 4C46CCBC1580469E00846B64 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 4C46CCC01580469E00846B64 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 4C46CCC11580469E00846B64 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 4C46CCC21580469E00846B64 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 4C48B46320E4D06900CF5F84 /* MergeSwift.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = MergeSwift.sh; path = "Build Scripts/MergeSwift.sh"; sourceTree = ""; }; 4C4A0D6C1C973E9000064A23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = "Application Properties/Info.plist"; sourceTree = ""; }; 4C51327720F76E980033B703 /* TLOLocalization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TLOLocalization.swift; path = ../../../Shared/Library/TLOLocalization.swift; sourceTree = ""; }; 4C52379215C18F6700414852 /* Style Default Templates */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Style Default Templates"; sourceTree = ""; }; 4C5274E020F8D78700B18F9D /* IRCConnectionErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = IRCConnectionErrors.h; path = ../../../Shared/Headers/IRCConnectionErrors.h; sourceTree = ""; }; 4C5274E520F8D7CD00B18F9D /* IRCConnectionErrors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = IRCConnectionErrors.m; path = ../../../Shared/IRC/IRCConnectionErrors.m; sourceTree = ""; }; 4C58B570186011AC00834882 /* Acknowledgements.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = Acknowledgements.pdf; path = ../../Acknowledgements.pdf; sourceTree = SOURCE_ROOT; }; 4C5BA4E616F1302F00A96CA2 /* Textual.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Textual.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4C761BAE1E155CB100A505B7 /* BuildServices.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = BuildServices.sh; path = "Build Scripts/BuildServices.sh"; sourceTree = ""; }; 4C7961C6267D25B400D37F07 /* UpdateFeatureFlags.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = UpdateFeatureFlags.sh; path = "Build Scripts/UpdateFeatureFlags.sh"; sourceTree = ""; }; 4C83B9101BA9BEB800BD7718 /* Chat Filters.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = "Chat Filters.bundle"; path = "../../.tmp/SharedBuildProducts-Extensions/Chat Filters.bundle"; sourceTree = SOURCE_ROOT; }; 4C8878032C33706E0016DB98 /* TPCSandboxMigration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCSandboxMigration.m; sourceTree = ""; }; 4C8878062C3371470016DB98 /* TPCSandboxMigrationPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCSandboxMigrationPrivate.h; sourceTree = ""; }; 4C8F2F5D1AAE6467007821CC /* EncryptionKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = EncryptionKit.framework; path = "../../.tmp/SharedBuildProducts-Frameworks/EncryptionKit.framework"; sourceTree = SOURCE_ROOT; }; 4C8F3FD62C31AF6E00118AAF /* THOPluginManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = THOPluginManager.h; sourceTree = ""; }; 4C8F3FDB2C31B15200118AAF /* THOPluginItemLogging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = THOPluginItemLogging.m; sourceTree = ""; }; 4C956893194A8862003D97FD /* Smiley Converter.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = "Smiley Converter.bundle"; path = "../../.tmp/SharedBuildProducts-Extensions/Smiley Converter.bundle"; sourceTree = SOURCE_ROOT; }; 4C9A1D921836E6A100C14835 /* User Insights.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = "User Insights.bundle"; path = "../../.tmp/SharedBuildProducts-Extensions/User Insights.bundle"; sourceTree = SOURCE_ROOT; }; 4C9A1D931836E6A100C14835 /* System Info.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = "System Info.bundle"; path = "../../.tmp/SharedBuildProducts-Extensions/System Info.bundle"; sourceTree = SOURCE_ROOT; }; 4C9A1D941836E6A100C14835 /* ZNC Additions.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = "ZNC Additions.bundle"; path = "../../.tmp/SharedBuildProducts-Extensions/ZNC Additions.bundle"; sourceTree = SOURCE_ROOT; }; 4CA8EC621B63C2970087BF72 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 4CAB27502537EE43009B1F07 /* IRCChannelMemberList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IRCChannelMemberList.m; sourceTree = ""; }; 4CAB27562537EE66009B1F07 /* IRCChannelMemberListPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IRCChannelMemberListPrivate.h; sourceTree = ""; }; 4CAB275D2537F71B009B1F07 /* IRCChannelMemberList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IRCChannelMemberList.h; sourceTree = ""; }; 4CAB2764253863BF009B1F07 /* IRCChannelMemberListController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IRCChannelMemberListController.m; sourceTree = ""; }; 4CAB276A253863D9009B1F07 /* IRCChannelMemberListControllerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IRCChannelMemberListControllerPrivate.h; sourceTree = ""; }; 4CB638CB2D45A06C002B2CC3 /* FeatureFlags.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = FeatureFlags.h; path = "../../.tmp/Build Headers/FeatureFlags.h"; sourceTree = SOURCE_ROOT; }; 4CB873D622B4A497005AB046 /* TDCPreferencesUserStyleSheetPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TDCPreferencesUserStyleSheetPrivate.h; sourceTree = ""; }; 4CB873DB22B4A56B005AB046 /* TDCPreferencesUserStyleSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TDCPreferencesUserStyleSheet.m; sourceTree = ""; }; 4CC4A7371F8A1404008FF15F /* GRMustache.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GRMustache.framework; path = "../../Frameworks/Static Libraries/Libraries/GRMustache.framework"; sourceTree = SOURCE_ROOT; }; 4CC70202171E1CB400BDFAE0 /* BuildConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BuildConfig.h; path = "../../.tmp/Build Headers/BuildConfig.h"; sourceTree = SOURCE_ROOT; }; 4CCD93FC1CA7291300335381 /* JavaScript */ = {isa = PBXFileReference; lastKnownFileType = folder; path = JavaScript; sourceTree = ""; }; 4CCE521A18C7BE0600D49601 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 4CD6F7BB1CA7747600842597 /* ExportArchive.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = ExportArchive.sh; path = "Build Scripts/ExportArchive.sh"; sourceTree = ""; }; 4CEFEDAF22B4B6E8002CEE19 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TDCPreferencesUserStyleSheet.strings; sourceTree = ""; }; 4CF4D6B619837C1700FB5AF0 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 4CF76A371A91153A0088BF9A /* AutoHyperlinks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AutoHyperlinks.framework; path = "../../.tmp/SharedBuildProducts-Frameworks/AutoHyperlinks.framework"; sourceTree = SOURCE_ROOT; }; 4CF76A391A91153A0088BF9A /* CocoaExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaExtensions.framework; path = "../../.tmp/SharedBuildProducts-Frameworks/CocoaExtensions.framework"; sourceTree = SOURCE_ROOT; }; 5D9B8EB6170A0EB400919CB0 /* BuildFrameworks.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = BuildFrameworks.sh; path = "Build Scripts/BuildFrameworks.sh"; sourceTree = ""; }; 5D9B8EB8170A0EB400919CB0 /* UpdateVersionInfo.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = UpdateVersionInfo.sh; path = "Build Scripts/UpdateVersionInfo.sh"; sourceTree = ""; }; 5D9B8EB9170A10F200919CB0 /* BuildExtensions.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = BuildExtensions.sh; path = "Build Scripts/BuildExtensions.sh"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 4C1DC54F1580420500A47BC9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4C06E48820EC542C0055D09A /* AudioToolbox.framework in Frameworks */, 4C06E48920EC542C0055D09A /* Cocoa.framework in Frameworks */, 4C06E48A20EC542C0055D09A /* CoreServices.framework in Frameworks */, 4C06E48C20EC542C0055D09A /* IOKit.framework in Frameworks */, 4C06E48E20EC542C0055D09A /* QuartzCore.framework in Frameworks */, 4C2F42062C3F4218000D962D /* UserNotifications.framework in Frameworks */, 4C06E48F20EC542C0055D09A /* Security.framework in Frameworks */, 4C06E49220EC542C0055D09A /* SystemConfiguration.framework in Frameworks */, 4C06E49320EC542C0055D09A /* WebKit.framework in Frameworks */, 4C06E48120EC54260055D09A /* AutoHyperlinks.framework in Frameworks */, 4C06E48220EC54260055D09A /* CocoaExtensions.framework in Frameworks */, 4C06E48320EC54260055D09A /* EncryptionKit.framework in Frameworks */, 4C06E48420EC54260055D09A /* GRMustache.framework in Frameworks */, 4C06E48720EC54260055D09A /* Sparkle.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 4C5BA45716F1302F00A96CA2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4C06E42920EC53620055D09A /* AudioToolbox.framework in Frameworks */, 4C06E43B20EC539E0055D09A /* GRMustache.framework in Frameworks */, 4C06E42A20EC53620055D09A /* Cocoa.framework in Frameworks */, 4C06E42B20EC53620055D09A /* CoreServices.framework in Frameworks */, 4C06E43520EC539E0055D09A /* AutoHyperlinks.framework in Frameworks */, 4C2F42052C3F4218000D962D /* UserNotifications.framework in Frameworks */, 4C06E42D20EC53620055D09A /* IOKit.framework in Frameworks */, 4C06E44120EC539E0055D09A /* Sparkle.framework in Frameworks */, 4C06E43920EC539E0055D09A /* EncryptionKit.framework in Frameworks */, 4C06E42F20EC53620055D09A /* QuartzCore.framework in Frameworks */, 4C06E43020EC53620055D09A /* Security.framework in Frameworks */, 4C06E43320EC53620055D09A /* SystemConfiguration.framework in Frameworks */, 4C06E43720EC539E0055D09A /* CocoaExtensions.framework in Frameworks */, 4C06E43420EC53620055D09A /* WebKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4C06E13620EC4AC40055D09A /* User Interface */ = { isa = PBXGroup; children = ( 4C06E17B20EC4AC40055D09A /* Appearance */, 4C06E17120EC4AC40055D09A /* TDCAboutDialog.xib */, 4C06E16720EC4AC40055D09A /* TDCAddressBookSheet.xib */, 4C06E14920EC4AC40055D09A /* TDCChannelBanListSheet.xib */, 4C06E15F20EC4AC40055D09A /* TDCChannelInviteSheet.xib */, 4C06E14120EC4AC40055D09A /* TDCChannelModifyModesSheet.xib */, 4C06E16F20EC4AC40055D09A /* TDCChannelModifyTopicSheet.xib */, 4C06E14B20EC4AC40055D09A /* TDCChannelPropertiesSheet.xib */, 4C06E14320EC4AC40055D09A /* TDCChannelSpotlightController.xib */, 4C06E14520EC4AC40055D09A /* TDCFileTransferDialog.xib */, 4C06E17320EC4AC40055D09A /* TDCHighlightEntrySheet.xib */, 4C06E17720EC4AC40055D09A /* TDCLicenseManagerDialog.xib */, 4C06E14D20EC4AC40055D09A /* TDCLicenseManagerMigrateAppStoreSheet.xib */, 4C06E17920EC4AC40055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.xib */, 4C06E15B20EC4AC40055D09A /* TDCLicenseUpgradeActivateSheet.xib */, 4C06E16B20EC4AC40055D09A /* TDCLicenseUpgradeDialog.xib */, 4C06E15120EC4AC40055D09A /* TDCLicenseUpgradeEligibilitySheet.xib */, 4C06E16120EC4AC40055D09A /* TDCNicknameColorSheet.xib */, 4C06E15D20EC4AC40055D09A /* TDCPreferences.xib */, 4C17305222B4A94000DC5836 /* TDCPreferencesUserStyleSheet.xib */, 4C06E16520EC4AC40055D09A /* TDCProgressIndicatorSheet.xib */, 4C06E16D20EC4AC40055D09A /* TDCServerChangeNicknameSheet.xib */, 4C06E14720EC4AC40055D09A /* TDCServerChannelListDialog.xib */, 4C06E13F20EC4AC40055D09A /* TDCServerEndpointListSheet.xib */, 4C06E13D20EC4AC40055D09A /* TDCServerHighlightListSheet.xib */, 4C06E13920EC4AC40055D09A /* TDCServerPropertiesSheet.xib */, 4C06E16320EC4AC40055D09A /* TDCWelcomeSheet.xib */, 4C06E16920EC4AC40055D09A /* TVCAlert.xib */, 4C06E15520EC4AC40055D09A /* TVCChannelSelectionView.xib */, 4C06E17520EC4AC40055D09A /* TVCMainWindow.xib */, 4C06E15920EC4AC40055D09A /* TVCNotificationConfigurationView.xib */, 4C06E15720EC4AC40055D09A /* TXCMainMenu.xib */, ); path = "User Interface"; sourceTree = ""; }; 4C06E17B20EC4AC40055D09A /* Appearance */ = { isa = PBXGroup; children = ( 4C06E18120EC4AC40055D09A /* TDCChannelSpotlightAppearance.plist */, 4C06E17E20EC4AC40055D09A /* TDCChannelSpotlightAppearanceTemplate.plist */, 4C06E18220EC4AC40055D09A /* TVCMainWindowAppearance.plist */, 4C06E18520EC4AC40055D09A /* TVCMainWindowAppearanceTemplate.plist */, 4C06E18320EC4AC40055D09A /* TVCMainWindowTextViewAppearance.plist */, 4C06E18020EC4AC40055D09A /* TVCMainWindowTextViewAppearanceTemplate.plist */, 4C06E17C20EC4AC40055D09A /* TVCMemberListAppearance.plist */, 4C06E17F20EC4AC40055D09A /* TVCMemberListAppearanceTemplate.plist */, 4C06E18420EC4AC40055D09A /* TVCServerListAppearance.plist */, 4C06E17D20EC4AC40055D09A /* TVCServerListAppearanceTemplate.plist */, ); path = Appearance; sourceTree = ""; }; 4C06E2A620EC4FF00055D09A /* Language Files */ = { isa = PBXGroup; children = ( 4C06E2B120EC4FF00055D09A /* Accessibility.strings */, 4C06E2AF20EC4FF00055D09A /* BasicLanguage.strings */, 4C06E2AB20EC4FF00055D09A /* CommonErrors.strings */, 4C06E2C120EC4FF00055D09A /* IRC.strings */, 4C06E2C920EC4FF00055D09A /* Notifications.strings */, 4C06E2A920EC4FF00055D09A /* OffTheRecord.strings */, 4C06E2A720EC4FF00055D09A /* Prompts.strings */, 4C06E2BD20EC4FF00055D09A /* TDCAboutDialog.strings */, 4C06E2D520EC4FF00055D09A /* TDCAddressBookSheet.strings */, 4C06E2B520EC4FF00055D09A /* TDCChannelBanListSheet.strings */, 4C06E2BB20EC4FF00055D09A /* TDCChannelInviteSheet.strings */, 4C06E2C520EC4FF00055D09A /* TDCChannelModifyModesSheet.strings */, 4C06E2D920EC4FF00055D09A /* TDCChannelModifyTopicSheet.strings */, 4C06E2B320EC4FF00055D09A /* TDCChannelPropertiesSheet.strings */, 4C06E2D320EC4FF00055D09A /* TDCChannelSpotlightController.strings */, 4C06E2B920EC4FF00055D09A /* TDCFileTransferDialog.strings */, 4C06E2D720EC4FF00055D09A /* TDCLicenseUpgradeEligibilitySheet.strings */, 4C06E2B720EC4FF00055D09A /* TDCPreferencesController.strings */, 4CEFEDAE22B4B6E8002CEE19 /* TDCPreferencesUserStyleSheet.strings */, 4C06E2D120EC4FF00055D09A /* TDCServerChannelListDialog.strings */, 4C06E2CF20EC4FF00055D09A /* TDCServerEndpointListSheet.strings */, 4C06E2BF20EC4FF00055D09A /* TDCServerPropertiesSheet.strings */, 4C06E2C320EC4FF00055D09A /* TLOLicenseManager.strings */, 4C06E2C720EC4FF00055D09A /* TVCMainWindow.strings */, 4C06E2CD20EC4FF00055D09A /* TVCNotificationConfigurationView.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C06E34D20EC50D40055D09A /* Services */ = { isa = PBXGroup; children = ( 4C06E34E20EC50D40055D09A /* ICLPayloadLocal.m */, ); path = Services; sourceTree = ""; }; 4C06E35A20EC51370055D09A /* Shared */ = { isa = PBXGroup; children = ( 4C06E36920EC51370055D09A /* Headers */, 4C06E36020EC51370055D09A /* Library */, 4C06E38A20EC51370055D09A /* Services */, 4C06E38420EC51370055D09A /* Views */, ); name = Shared; path = ../Shared; sourceTree = SOURCE_ROOT; }; 4C06E36020EC51370055D09A /* Library */ = { isa = PBXGroup; children = ( 4C06E36120EC51370055D09A /* External Libraries */, ); path = Library; sourceTree = ""; }; 4C06E36120EC51370055D09A /* External Libraries */ = { isa = PBXGroup; children = ( 4C06E36220EC51370055D09A /* Sockets */, ); path = "External Libraries"; sourceTree = ""; }; 4C06E36220EC51370055D09A /* Sockets */ = { isa = PBXGroup; children = ( 4C06E36320EC51370055D09A /* GCDAsyncSocket.m */, 4C06E36520EC51370055D09A /* GCDAsyncSocketExtensions.m */, ); path = Sockets; sourceTree = ""; }; 4C06E36920EC51370055D09A /* Headers */ = { isa = PBXGroup; children = ( 4C06E37520EC51370055D09A /* ICLPayload.h */, 4C06E36A20EC51370055D09A /* External Libraries */, 4C06E37120EC51370055D09A /* Internal */, 4C06E37720EC51370055D09A /* Private */, 4C06E37E20EC51370055D09A /* Services */, ); path = Headers; sourceTree = ""; }; 4C06E36A20EC51370055D09A /* External Libraries */ = { isa = PBXGroup; children = ( 4C06E36C20EC51370055D09A /* GCDAsyncSocket.h */, 4C06E36B20EC51370055D09A /* GCDAsyncSocketExtensions.h */, ); path = "External Libraries"; sourceTree = ""; }; 4C06E37120EC51370055D09A /* Internal */ = { isa = PBXGroup; children = ( 4C06E37220EC51370055D09A /* ICLPayloadInternal.h */, ); path = Internal; sourceTree = ""; }; 4C06E37720EC51370055D09A /* Private */ = { isa = PBXGroup; children = ( 4C06E41020EC52240055D09A /* TVCLogLineXPCPrivate.h */, ); path = Private; sourceTree = ""; }; 4C06E37E20EC51370055D09A /* Services */ = { isa = PBXGroup; children = ( 4C06E37F20EC51370055D09A /* RCMConnectionManagerProtocol.h */, 4C06E38020EC51370055D09A /* ICLInlineContentProtocol.h */, 4C06E38120EC51370055D09A /* HLSHistoricLogProtocol.h */, ); path = Services; sourceTree = ""; }; 4C06E38420EC51370055D09A /* Views */ = { isa = PBXGroup; children = ( 4C06E38520EC51370055D09A /* Channel View */, ); path = Views; sourceTree = ""; }; 4C06E38520EC51370055D09A /* Channel View */ = { isa = PBXGroup; children = ( 4C06E38620EC51370055D09A /* TVCLogLineXPC.m */, ); path = "Channel View"; sourceTree = ""; }; 4C06E38A20EC51370055D09A /* Services */ = { isa = PBXGroup; children = ( 4C06E38B20EC51370055D09A /* ICLPayload.m */, ); path = Services; sourceTree = ""; }; 4C1DC5471580420500A47BC9 = { isa = PBXGroup; children = ( 4C31511220EB673E00448776 /* Classes */, 4C3152EC20EB676400448776 /* Resources */, 4C1DC5551580420500A47BC9 /* Frameworks */, 4C1DC5531580420500A47BC9 /* Products */, ); sourceTree = ""; }; 4C1DC5531580420500A47BC9 /* Products */ = { isa = PBXGroup; children = ( 4C1DC5521580420500A47BC9 /* Textual.app */, 4C5BA4E616F1302F00A96CA2 /* Textual.app */, ); name = Products; sourceTree = ""; }; 4C1DC5551580420500A47BC9 /* Frameworks */ = { isa = PBXGroup; children = ( 4C46CCCC158046BE00846B64 /* External Frameworks */, 4C211D3F15BF1F5E00E218DA /* External Libraries */, 4C46CCB91580469200846B64 /* System Frameworks */, ); name = Frameworks; sourceTree = ""; }; 4C211D3F15BF1F5E00E218DA /* External Libraries */ = { isa = PBXGroup; children = ( ); name = "External Libraries"; sourceTree = ""; }; 4C31511220EB673E00448776 /* Classes */ = { isa = PBXGroup; children = ( 4C31529A20EB673E00448776 /* Controllers */, 4C31526220EB673E00448776 /* Dialogs */, 4C31516020EB673E00448776 /* Headers */, 4C3152D920EB673E00448776 /* Helpers */, 4C31512220EB673E00448776 /* IRC */, 4C31514520EB673E00448776 /* Library */, 4C31514320EB673E00448776 /* Others */, 4C31511320EB673E00448776 /* Preferences */, 4C06E34D20EC50D40055D09A /* Services */, 4C06E35A20EC51370055D09A /* Shared */, 4C3152A220EB673E00448776 /* Views */, ); path = Classes; sourceTree = ""; }; 4C31511320EB673E00448776 /* Preferences */ = { isa = PBXGroup; children = ( 4C31511E20EB673E00448776 /* Themes */, 4C31511420EB673E00448776 /* TPCApplicationInfo.m */, 4C31511A20EB673E00448776 /* TPCPathInfo.m */, 4C06E28820EC4F000055D09A /* TPCPreferences.m */, 4C31511520EB673E00448776 /* TPCPreferencesLocal.m */, 4C31511620EB673E00448776 /* TPCPreferencesImportExport.m */, 4C31512120EB673E00448776 /* TPCPreferencesReload.m */, 4C06E28920EC4F000055D09A /* TPCPreferencesUserDefaults.m */, 4C31511920EB673E00448776 /* TPCPreferencesUserDefaultsLocal.m */, 4C31511820EB673E00448776 /* TPCResourceManager.m */, 4C8878032C33706E0016DB98 /* TPCSandboxMigration.m */, ); path = Preferences; sourceTree = ""; }; 4C31511E20EB673E00448776 /* Themes */ = { isa = PBXGroup; children = ( 4C31512020EB673E00448776 /* TPCTheme.m */, 4C31511F20EB673E00448776 /* TPCThemeController.m */, ); path = Themes; sourceTree = ""; }; 4C31512220EB673E00448776 /* IRC */ = { isa = PBXGroup; children = ( 4C31512320EB673E00448776 /* IRCAddressBook.m */, 4C31512B20EB673E00448776 /* IRCAddressBookMatchCache.m */, 4C31513E20EB673E00448776 /* IRCAddressBookUserTracking.m */, 4C31512820EB673E00448776 /* IRCChannel.m */, 4C31512420EB673E00448776 /* IRCChannelConfig.m */, 4CAB27502537EE43009B1F07 /* IRCChannelMemberList.m */, 4CAB2764253863BF009B1F07 /* IRCChannelMemberListController.m */, 4C31514020EB673E00448776 /* IRCChannelMode.m */, 4C31513B20EB673E00448776 /* IRCClient.m */, 4C31513C20EB673E00448776 /* IRCClientConfig.m */, 4C31513820EB673E00448776 /* IRCClientRequestedCommands.m */, 4C31513720EB673E00448776 /* IRCCommandIndex.m */, 4C31512E20EB673E00448776 /* IRCConnection.m */, 4C06E29C20EC4F880055D09A /* IRCConnectionConfig.m */, 4C5274E520F8D7CD00B18F9D /* IRCConnectionErrors.m */, 4C31513120EB673E00448776 /* IRCExtras.m */, 4C31512620EB673E00448776 /* IRCHighlightLogEntry.m */, 4C31512A20EB673E00448776 /* IRCHighlightMatchCondition.m */, 4C31514120EB673E00448776 /* IRCISupportInfo.m */, 4C31512D20EB673E00448776 /* IRCMessage.m */, 4C31513F20EB673E00448776 /* IRCMessageBatch.m */, 4C31513020EB673E00448776 /* IRCModeInfo.m */, 4C31513920EB673E00448776 /* IRCNetworkList.m */, 4C31512520EB673E00448776 /* IRCPrefix.m */, 4C31512C20EB673E00448776 /* IRCSendingMessage.m */, 4C31512920EB673E00448776 /* IRCServer.m */, 4C31512F20EB673E00448776 /* IRCTimerCommand.m */, 4C31514220EB673E00448776 /* IRCTreeItem.m */, 4C31513A20EB673E00448776 /* IRCWorld.m */, 4C31513220EB673E00448776 /* Users */, ); path = IRC; sourceTree = ""; }; 4C31513220EB673E00448776 /* Users */ = { isa = PBXGroup; children = ( 4C31513D20EB673E00448776 /* IRCChannelUser.m */, 4C31513320EB673E00448776 /* IRCUserRelations.m */, 4C31513420EB673E00448776 /* IRCUser.m */, 4C31513520EB673E00448776 /* IRCUserNicknameColorStyleGenerator.m */, 4C31513620EB673E00448776 /* IRCUserPersistentStore.m */, ); path = Users; sourceTree = ""; }; 4C31514320EB673E00448776 /* Others */ = { isa = PBXGroup; children = ( 4C31514420EB673E00448776 /* main.m */, ); path = Others; sourceTree = ""; }; 4C31514520EB673E00448776 /* Library */ = { isa = PBXGroup; children = ( 4C31515E20EB673E00448776 /* Color Formatting */, 4C31514620EB673E00448776 /* External Libraries */, 4C31514C20EB673E00448776 /* License Manager */, 4C31515320EB673E00448776 /* TLOEncryptionManager.m */, 4C31515B20EB673E00448776 /* TLOFileLogger.m */, 4C31515420EB673E00448776 /* TLOInputHistory.m */, 4C31515620EB673E00448776 /* TLOInternetAddressLookup.m */, 4C31514B20EB673E00448776 /* TLOKeyEventHandler.m */, 4C31515520EB673E00448776 /* TLOLinkParser.swift */, 4C06E29220EC4F570055D09A /* TLOLocalization.m */, 4C51327720F76E980033B703 /* TLOLocalization.swift */, 4C31515C20EB673E00448776 /* TLONicknameCompletionStatus.m */, 4C31515D20EB673E00448776 /* TLONotificationConfiguration.m */, 4C31514920EB673E00448776 /* TLONotificationController.m */, 4C31515920EB673E00448776 /* TLOpenLink.swift */, 4C31515820EB673E00448776 /* TLOSoundPlayer.m */, 4C31515720EB673E00448776 /* TLOSpeechSynthesizer.m */, 4C31515A20EB673E00448776 /* TLOSpokenNotification.m */, 4C06E29320EC4F570055D09A /* TLOTimer.m */, ); path = Library; sourceTree = ""; }; 4C31514620EB673E00448776 /* External Libraries */ = { isa = PBXGroup; children = ( 4C31514720EB673E00448776 /* Sockets */, ); path = "External Libraries"; sourceTree = ""; }; 4C31514720EB673E00448776 /* Sockets */ = { isa = PBXGroup; children = ( 4C31514820EB673E00448776 /* OELReachability.m */, ); path = Sockets; sourceTree = ""; }; 4C31514C20EB673E00448776 /* License Manager */ = { isa = PBXGroup; children = ( 4C31514D20EB673E00448776 /* Standalone */, ); path = "License Manager"; sourceTree = ""; }; 4C31514D20EB673E00448776 /* Standalone */ = { isa = PBXGroup; children = ( 4C31514E20EB673E00448776 /* TLOLicenseManager.m */, 4C31515020EB673E00448776 /* TLOLicenseManagerDownloader.m */, 4C31514F20EB673E00448776 /* TLOLicenseManagerLastGen.m */, ); path = Standalone; sourceTree = ""; }; 4C31515E20EB673E00448776 /* Color Formatting */ = { isa = PBXGroup; children = ( 4C31515F20EB673E00448776 /* IRCColorFormat.m */, ); path = "Color Formatting"; sourceTree = ""; }; 4C31516020EB673E00448776 /* Headers */ = { isa = PBXGroup; children = ( 4C31516120EB673E00448776 /* External Libraries */, 4C31518420EB673E00448776 /* Internal */, 4C3151AB20EB673E00448776 /* Private */, 4C31524120EB673E00448776 /* IRC.h */, 4C3151A720EB673E00448776 /* IRCAddressBook.h */, 4C31517620EB673E00448776 /* IRCAddressBookUserTracking.h */, 4C31524B20EB673E00448776 /* IRCChannel.h */, 4C3151A420EB673E00448776 /* IRCChannelConfig.h */, 4CAB275D2537F71B009B1F07 /* IRCChannelMemberList.h */, 4C31519A20EB673E00448776 /* IRCChannelMode.h */, 4C31517920EB673E00448776 /* IRCChannelUser.h */, 4C31517C20EB673E00448776 /* IRCClient.h */, 4C31517F20EB673E00448776 /* IRCClientConfig.h */, 4C31526120EB673E00448776 /* IRCColorFormat.h */, 4C31517120EB673E00448776 /* IRCCommandIndex.h */, 4C31526020EB673E00448776 /* IRCConnection.h */, 4C06E24720EC4B970055D09A /* IRCConnectionConfig.h */, 4C5274E020F8D78700B18F9D /* IRCConnectionErrors.h */, 4C3151A120EB673E00448776 /* IRCHighlightLogEntry.h */, 4C31524720EB673E00448776 /* IRCHighlightMatchCondition.h */, 4C31519820EB673E00448776 /* IRCISupportInfo.h */, 4C31525320EB673E00448776 /* IRCMessage.h */, 4C31516C20EB673E00448776 /* IRCModeInfo.h */, 4C31518120EB673E00448776 /* IRCNetworkList.h */, 4C31516620EB673E00448776 /* IRCNumerics.h */, 4C3151A520EB673E00448776 /* IRCPrefix.h */, 4C31525520EB673E00448776 /* IRCSendingMessage.h */, 4C31524620EB673E00448776 /* IRCServer.h */, 4C31519720EB673E00448776 /* IRCTreeItem.h */, 4C31524E20EB673E00448776 /* IRCUser.h */, 4C31524F20EB673E00448776 /* IRCUserRelations.h */, 4C31518020EB673E00448776 /* IRCWorld.h */, 4C31519E20EB673E00448776 /* NSColorHelper.h */, 4C31525820EB673E00448776 /* NSStringHelper.h */, 4C31524320EB673E00448776 /* NSViewHelper.h */, 4C06E26F20EC4E350055D09A /* StaticDefinitions.h */, 4C31524920EB673E00448776 /* TDCAlert.h */, 4C31516A20EB673E00448776 /* TDCInputPrompt.h */, 4C31518220EB673E00448776 /* TDCSheetBase.h */, 4C31525C20EB673E00448776 /* TDCWindowBase.h */, 4C31525F20EB673E00448776 /* Textual.h */, 4C31517720EB673E00448776 /* TextualApplication.h */, 4C8F3FD62C31AF6E00118AAF /* THOPluginManager.h */, 4C31517820EB673E00448776 /* THOPluginProtocol.h */, 4C3151A620EB673E00448776 /* THOUnicodeHelper.h */, 4C31524520EB673E00448776 /* TLOEncryptionManager.h */, 4C31525B20EB673E00448776 /* TLOInternetAddressLookup.h */, 4C3151A820EB673E00448776 /* TLOKeyEventHandler.h */, 4C31519D20EB673E00448776 /* TLOLinkParser.h */, 4C06E27720EC4E860055D09A /* TLOLocalization.h */, 4C3151AA20EB673E00448776 /* TLONotificationController.h */, 4C31517B20EB673E00448776 /* TLOpenLink.h */, 4C31516420EB673E00448776 /* TLOSoundPlayer.h */, 4C06E27420EC4E850055D09A /* TLOTimer.h */, 4C31524020EB673E00448776 /* TPCApplicationInfo.h */, 4C31517D20EB673E00448776 /* TPCPathInfo.h */, 4C06E27620EC4E860055D09A /* TPCPreferences.h */, 4C31524A20EB673E00448776 /* TPCPreferencesImportExport.h */, 4C31516820EB673E00448776 /* TPCPreferencesLocal.h */, 4C31519C20EB673E00448776 /* TPCPreferencesReload.h */, 4C06E27520EC4E850055D09A /* TPCPreferencesUserDefaults.h */, 4C31519F20EB673E00448776 /* TPCPreferencesUserDefaultsLocal.h */, 4C31516520EB673E00448776 /* TPCResourceManager.h */, 4C31517020EB673E00448776 /* TPCTheme.h */, 4C31517220EB673E00448776 /* TPCThemeController.h */, 4C31516F20EB673E00448776 /* TVCAlert.h */, 4C31516E20EB673E00448776 /* TVCAppearance.h */, 4C31517E20EB673E00448776 /* TVCAutoExpandingTextField.h */, 4C3151A320EB673E00448776 /* TVCAutoExpandingTokenField.h */, 4C3151A220EB673E00448776 /* TVCBasicTableView.h */, 4C31519620EB673E00448776 /* TVCChannelSelectionViewController.h */, 4C31525620EB673E00448776 /* TVCLogController.h */, 4C31519920EB673E00448776 /* TVCLogLine.h */, 4C31525420EB673E00448776 /* TVCLogRenderer.h */, 4C31524D20EB673E00448776 /* TVCLogView.h */, 4C31524C20EB673E00448776 /* TVCMainWindow.h */, 4C31525A20EB673E00448776 /* TVCMainWindowAppearance.h */, 4C31525720EB673E00448776 /* TVCMainWindowLoadingScreen.h */, 4C31525220EB673E00448776 /* TVCMainWindowSplitView.h */, 4C31525920EB673E00448776 /* TVCMainWindowTextView.h */, 4C31524220EB673E00448776 /* TVCMainWindowTextViewAppearance.h */, 4C31516920EB673E00448776 /* TVCMemberList.h */, 4C31525120EB673E00448776 /* TVCMemberListAppearance.h */, 4C31516720EB673E00448776 /* TVCServerList.h */, 4C31516D20EB673E00448776 /* TVCServerListAppearance.h */, 4C31524420EB673E00448776 /* TVCTextViewWithIRCFormatter.h */, 4C31525020EB673E00448776 /* TVCValidatedComboBox.h */, 4C31525E20EB673E00448776 /* TVCValidatedTextField.h */, 4C31517320EB673E00448776 /* TXAppearance.h */, 4C31517420EB673E00448776 /* TXAppearanceHelper.h */, 4C31524820EB673E00448776 /* TXGlobalModels.h */, 4C31517A20EB673E00448776 /* TXMasterController.h */, 4C31519B20EB673E00448776 /* TXMenuController.h */, 4C31516B20EB673E00448776 /* TXSharedApplication.h */, ); path = Headers; sourceTree = ""; }; 4C31516120EB673E00448776 /* External Libraries */ = { isa = PBXGroup; children = ( 4C31516220EB673E00448776 /* GTMEncodeHTML.h */, 4C31516320EB673E00448776 /* OELReachability.h */, ); path = "External Libraries"; sourceTree = ""; }; 4C31518420EB673E00448776 /* Internal */ = { isa = PBXGroup; children = ( 4C31518720EB673E00448776 /* IRCAddressBookInternal.h */, 4C31518C20EB673E00448776 /* IRCChannelConfigInternal.h */, 4C31518F20EB673E00448776 /* IRCChannelUserInternal.h */, 4C31518D20EB673E00448776 /* IRCClientConfigInternal.h */, 4C06E25B20EC4BBD0055D09A /* IRCConnectionConfigInternal.h */, 4C31519420EB673E00448776 /* IRCHighlightLogEntryInternal.h */, 4C31519520EB673E00448776 /* IRCHighlightMatchConditionInternal.h */, 4C31519120EB673E00448776 /* IRCMessageInternal.h */, 4C31519020EB673E00448776 /* IRCModeInfoInternal.h */, 4C31518B20EB673E00448776 /* IRCPrefixInternal.h */, 4C31518520EB673E00448776 /* IRCServerInternal.h */, 4C31518620EB673E00448776 /* IRCUserInternal.h */, 4C31518820EB673E00448776 /* TDCChannelPropertiesSheetInternal.h */, 4C31518920EB673E00448776 /* TDCChannelSpotlightAppearanceInternal.h */, 4C31519320EB673E00448776 /* TDCChannelSpotlightControllerInternal.h */, 4C31519220EB673E00448776 /* TDCFileTransferDialogInternal.h */, 4C31518E20EB673E00448776 /* TVCLogLineInternal.h */, ); path = Internal; sourceTree = ""; }; 4C3151AB20EB673E00448776 /* Private */ = { isa = PBXGroup; children = ( 4C06E41520EC52D50055D09A /* ICLPayloadLocalPrivate.h */, 4C3151E020EB673E00448776 /* IRCAddressBookMatchCachePrivate.h */, 4C3151EA20EB673E00448776 /* IRCAddressBookUserTrackingPrivate.h */, 4C31522120EB673E00448776 /* IRCChannelConfigPrivate.h */, 4CAB276A253863D9009B1F07 /* IRCChannelMemberListControllerPrivate.h */, 4CAB27562537EE66009B1F07 /* IRCChannelMemberListPrivate.h */, 4C31522920EB673E00448776 /* IRCChannelModePrivate.h */, 4C31520220EB673E00448776 /* IRCChannelPrivate.h */, 4C31521120EB673E00448776 /* IRCChannelUserPrivate.h */, 4C3151C720EB673E00448776 /* IRCClientConfigPrivate.h */, 4C31520B20EB673E00448776 /* IRCClientPrivate.h */, 4C31523F20EB673E00448776 /* IRCClientRequestedCommandsPrivate.h */, 4C3151B120EB673E00448776 /* IRCColorFormatPrivate.h */, 4C3151AE20EB673E00448776 /* IRCCommandIndexPrivate.h */, 4C31522020EB673E00448776 /* IRCConnectionPrivate.h */, 4C3151F620EB673E00448776 /* IRCExtrasPrivate.h */, 4C3151B220EB673E00448776 /* IRCHighlightLogEntryPrivate.h */, 4C3151ED20EB673E00448776 /* IRCISupportInfoPrivate.h */, 4C3151E120EB673E00448776 /* IRCMessageBatchPrivate.h */, 4C3151BE20EB673E00448776 /* IRCMessagePrivate.h */, 4C3151C120EB673E00448776 /* IRCServerPrivate.h */, 4C3151B820EB673E00448776 /* IRCTimerCommandPrivate.h */, 4C3151E320EB673E00448776 /* IRCTreeItemPrivate.h */, 4C3151FF20EB673E00448776 /* IRCUserNicknameColorStyleGeneratorPrivate.h */, 4C3151BC20EB673E00448776 /* IRCUserPersistentStorePrivate.h */, 4C3151E720EB673E00448776 /* IRCUserPrivate.h */, 4C3151B420EB673E00448776 /* IRCUserRelationsPrivate.h */, 4C3151DB20EB673E00448776 /* IRCWorldPrivate.h */, 4C06E26220EC4C560055D09A /* NSObjectHelperPrivate.h */, 4C3151E220EB673E00448776 /* NSTableVIewHelperPrivate.h */, 4C31521420EB673E00448776 /* NSViewHelperPrivate.h */, 4C31520A20EB673E00448776 /* SwiftBridgingHeaderPrivate.h */, 4C31522820EB673E00448776 /* TDCAboutDialogPrivate.h */, 4C3151BA20EB673E00448776 /* TDCAddressBookSheetPrivate.h */, 4C3151B920EB673E00448776 /* TDCChannelBanListSheetPrivate.h */, 4C31523E20EB673E00448776 /* TDCChannelInviteSheetPrivate.h */, 4C31523D20EB673E00448776 /* TDCChannelModifyModesSheetPrivate.h */, 4C3151E820EB673E00448776 /* TDCChannelModifyTopicSheetPrivate.h */, 4C3151B520EB673E00448776 /* TDCChannelPropertiesNotificationConfigurationPrivate.h */, 4C31523B20EB673E00448776 /* TDCChannelPropertiesSheetPrivate.h */, 4C31521B20EB673E00448776 /* TDCChannelSpotlightAppearancePrivate.h */, 4C31522B20EB673E00448776 /* TDCChannelSpotlightControllerPrivate.h */, 4C3151D120EB673E00448776 /* TDCChannelSpotlightControlsPrivate.h */, 4C3151D920EB673E00448776 /* TDCChannelSpotlightSearchResultPrivate.h */, 4C31522C20EB673E00448776 /* TDCChannelSpotlightSearchResultsTablePrivate.h */, 4C31521220EB673E00448776 /* TDCFileTransferDialogPrivate.h */, 4C3151BD20EB673E00448776 /* TDCFileTransferDialogTableCellPrivate.h */, 4C31520720EB673E00448776 /* TDCFileTransferDialogTransferControllerPrivate.h */, 4C3151D720EB673E00448776 /* TDCHighlightEntrySheetPrivate.h */, 4C31523720EB673E00448776 /* TDCLicenseManagerDialogPrivate.h */, 4C3151FD20EB673E00448776 /* TDCLicenseManagerMigrateAppStoreSheetPrivate.h */, 4C31523C20EB673E00448776 /* TDCLicenseManagerRecoverLostLicenseSheetPrivate.h */, 4C31520E20EB673E00448776 /* TDCLicenseUpgradeActivateSheetPrivate.h */, 4C3151D320EB673E00448776 /* TDCLicenseUpgradeCommonActionsPrivate.h */, 4C3151CC20EB673E00448776 /* TDCLicenseUpgradeDialogPrivate.h */, 4C3151FC20EB673E00448776 /* TDCLicenseUpgradeEligibilitySheetPrivate.h */, 4C3151E620EB673E00448776 /* TDCNicknameColorSheetPrivate.h */, 4C3151F120EB673E00448776 /* TDCPreferencesControllerPrivate.h */, 4C31523920EB673E00448776 /* TDCPreferencesNotificationConfigurationPrivate.h */, 4CB873D622B4A497005AB046 /* TDCPreferencesUserStyleSheetPrivate.h */, 4C3151DC20EB673E00448776 /* TDCProgressIndicatorSheetPrivate.h */, 4C3151EF20EB673E00448776 /* TDCServerChangeNicknameSheetPrivate.h */, 4C3151C320EB673E00448776 /* TDCServerChannelListDialogPrivate.h */, 4C31522F20EB673E00448776 /* TDCServerEndpointListSheetPrivate.h */, 4C3151EB20EB673E00448776 /* TDCServerEndpointListSheetTablePrivate.h */, 4C31520F20EB673E00448776 /* TDCServerHighlightListSheetPrivate.h */, 4C3151E920EB673E00448776 /* TDCServerPropertiesSheetPrivate.h */, 4C3151FB20EB673E00448776 /* TDCSharedProtocolDefinitionsPrivate.h */, 4C3151C420EB673E00448776 /* TDCWelcomeSheetPrivate.h */, 4C3151D820EB673E00448776 /* TextualPrivate.h */, 4C31521620EB673E00448776 /* THOPluginDispatcherPrivate.h */, 4C31521920EB673E00448776 /* THOPluginItemPrivate.h */, 4C3151CF20EB673E00448776 /* THOPluginManagerPrivate.h */, 4C3151B320EB673E00448776 /* THOPluginProtocolPrivate.h */, 4C31520120EB673E00448776 /* TLOEncryptionManagerPrivate.h */, 4C3151F220EB673E00448776 /* TLOFileLoggerPrivate.h */, 4C3151DA20EB673E00448776 /* TLOInputHistoryPrivate.h */, 4C31520920EB673E00448776 /* TLOLicenseManagerDownloaderPrivate.h */, 4C31523320EB673E00448776 /* TLOLicenseManagerLastGenPrivate.h */, 4C31522220EB673E00448776 /* TLOLicenseManagerPrivate.h */, 4C31523520EB673E00448776 /* TLONicknameCompletionStatusPrivate.h */, 4C31523420EB673E00448776 /* TLONotificationConfigurationPrivate.h */, 4C3151AF20EB673E00448776 /* TLONotificationControllerPrivate.h */, 4C31523620EB673E00448776 /* TLOSpeechSynthesizerPrivate.h */, 4C31520320EB673E00448776 /* TLOSpokenNotificationPrivate.h */, 4C3151EE20EB673E00448776 /* TPCApplicationInfoPrivate.h */, 4C3151D220EB673E00448776 /* TPCPathInfoPrivate.h */, 4C3151E420EB673E00448776 /* TPCPreferencesImportExportPrivate.h */, 4C31521820EB673E00448776 /* TPCPreferencesLocalPrivate.h */, 4C06E26020EC4C560055D09A /* TPCPreferencesPrivate.h */, 4C06E26120EC4C560055D09A /* TPCPreferencesUserDefaultsPrivate.h */, 4C8878062C3371470016DB98 /* TPCSandboxMigrationPrivate.h */, 4C31522520EB673E00448776 /* TPCResourceManagerPrivate.h */, 4C31521020EB673E00448776 /* TPCThemeControllerPrivate.h */, 4C3151D620EB673E00448776 /* TPCThemePrivate.h */, 4C3151D520EB673E00448776 /* TVCAppearancePrivate.h */, 4C3151F820EB673E00448776 /* TVCChannelSelectionOutlineViewCellPrivate.h */, 4C31520520EB673E00448776 /* TVCChannelSelectionViewControllerPrivate.h */, 4C31521F20EB673E00448776 /* TVCContentNavigationOutlineViewPrivate.h */, 4C31522720EB673E00448776 /* TVCDockIconPrivate.h */, 4C31517520EB673E00448776 /* TVCErrorMessagePopoverControllerPrivate.h */, 4C3151A020EB673E00448776 /* TVCErrorMessagePopoverPrivate.h */, 4C31522420EB673E00448776 /* TVCLogControllerHistoricLogFilePrivate.h */, 4C3151C220EB673E00448776 /* TVCLogControllerInlineMediaServicePrivate.h */, 4C3151C920EB673E00448776 /* TVCLogControllerOperationQueuePrivate.h */, 4C31521520EB673E00448776 /* TVCLogControllerPrivate.h */, 4C31523020EB673E00448776 /* TVCLogLinePrivate.h */, 4C3151DD20EB673E00448776 /* TVCLogPolicyPrivate.h */, 4C3151B020EB673E00448776 /* TVCLogScriptEventSinkPrivate.h */, 4C31521D20EB673E00448776 /* TVCLogViewInternalWK1.h */, 4C3151F020EB673E00448776 /* TVCLogViewInternalWK2.h */, 4C31520D20EB673E00448776 /* TVCLogViewPrivate.h */, 4C31520020EB673E00448776 /* TVCMainWindowAppearancePrivate.h */, 4C3151C820EB673E00448776 /* TVCMainWindowChannelViewPrivate.h */, 4C3151F420EB673E00448776 /* TVCMainWindowLoadingScreenPrivate.h */, 4C31523820EB673E00448776 /* TVCMainWindowPrivate.h */, 4C31522D20EB673E00448776 /* TVCMainWindowSegmentedControlPrivate.h */, 4C3151CE20EB673E00448776 /* TVCMainWindowSplitViewPrivate.h */, 4C3151F320EB673E00448776 /* TVCMainWindowTextViewAppearancePrivate.h */, 4C3151BB20EB673E00448776 /* TVCMainWindowTextViewPrivate.h */, 4C3151DF20EB673E00448776 /* TVCMainWindowTitlebarAccessoryViewPrivate.h */, 4C31521A20EB673E00448776 /* TVCMemberListAppearancePrivate.h */, 4C3151C520EB673E00448776 /* TVCMemberListCellPrivate.h */, 4C3151BF20EB673E00448776 /* TVCMemberListPrivate.h */, 4C3151B620EB673E00448776 /* TVCMemberListUserInfoPopoverPrivate.h */, 4C31522320EB673E00448776 /* TVCNotificationConfigurationViewControllerPrivate.h */, 4C31521720EB673E00448776 /* TVCServerListAppearancePrivate.h */, 4C3151F720EB673E00448776 /* TVCServerListCellPrivate.h */, 4C3151CA20EB673E00448776 /* TVCServerListPrivate.h */, 4C31522620EB673E00448776 /* TVCTextFormatterMenuPrivate.h */, 4C31520820EB673E00448776 /* TVCTextViewWithIRCFormatterPrivate.h */, 4C3151CD20EB673E00448776 /* TVCWK1AutoScrollerPrivate.h */, 4C31523A20EB673E00448776 /* TXAppearancePrivate.h */, 4C31520620EB673E00448776 /* TXApplicationPrivate.h */, 4C31523220EB673E00448776 /* TXGlobalModelsPrivate.h */, 4C31520420EB673E00448776 /* TXMasterControllerPrivate.h */, 4C3151DE20EB673E00448776 /* TXMenuControllerPrivate.h */, 4C3151C620EB673E00448776 /* TXSharedApplicationPrivate.h */, 4C31522A20EB673E00448776 /* TXWindowControllerPrivate.h */, 4C3151AD20EB673E00448776 /* WebScriptObjectHelperPrivate.h */, 4C31523120EB673E00448776 /* WKWebViewPrivate.h */, ); path = Private; sourceTree = ""; }; 4C31526220EB673E00448776 /* Dialogs */ = { isa = PBXGroup; children = ( 4C31526A20EB673E00448776 /* Channel Spotlight */, 4C31529020EB673E00448776 /* File Transfers */, 4C31527220EB673E00448776 /* License Manager */, 4C31526720EB673E00448776 /* Preferences */, 4C31528D20EB673E00448776 /* Server Endpoint */, 4C31528120EB673E00448776 /* TDCAboutDialog.m */, 4C31527020EB673E00448776 /* TDCAddressBookSheet.m */, 4C31528020EB673E00448776 /* TDCAlert.m */, 4C31528220EB673E00448776 /* TDCChannelBanListSheet.m */, 4C31526320EB673E00448776 /* TDCChannelInviteSheet.m */, 4C31526620EB673E00448776 /* TDCChannelModifyModesSheet.m */, 4C31528920EB673E00448776 /* TDCChannelModifyTopicSheet.m */, 4C31527120EB673E00448776 /* TDCChannelPropertiesNotificationConfiguration.m */, 4C31526420EB673E00448776 /* TDCChannelPropertiesSheet.m */, 4C31528320EB673E00448776 /* TDCHighlightEntrySheet.m */, 4C31528520EB673E00448776 /* TDCInputPrompt.m */, 4C31529420EB673E00448776 /* TDCNicknameColorSheet.m */, 4C31528820EB673E00448776 /* TDCProgressIndicatorSheet.m */, 4C31528C20EB673E00448776 /* TDCServerChangeNicknameSheet.m */, 4C31528620EB673E00448776 /* TDCServerChannelListDialog.m */, 4C31528720EB673E00448776 /* TDCServerHighlightListSheet.m */, 4C31526520EB673E00448776 /* TDCServerPropertiesSheet.m */, 4C31528A20EB673E00448776 /* TDCSheetBase.m */, 4C31528B20EB673E00448776 /* TDCWelcomeSheet.m */, 4C31528420EB673E00448776 /* TDCWindowBase.m */, ); path = Dialogs; sourceTree = ""; }; 4C31526720EB673E00448776 /* Preferences */ = { isa = PBXGroup; children = ( 4C31526920EB673E00448776 /* TDCPreferencesController.m */, 4C31526820EB673E00448776 /* TDCPreferencesNotificationConfiguration.m */, 4CB873DB22B4A56B005AB046 /* TDCPreferencesUserStyleSheet.m */, ); path = Preferences; sourceTree = ""; }; 4C31526A20EB673E00448776 /* Channel Spotlight */ = { isa = PBXGroup; children = ( 4C31526E20EB673E00448776 /* TDCChannelSpotlightAppearance.m */, 4C31526D20EB673E00448776 /* TDCChannelSpotlightController.m */, 4C31526B20EB673E00448776 /* TDCChannelSpotlightControls.m */, 4C31526C20EB673E00448776 /* TDCChannelSpotlightSearchResult.m */, 4C31526F20EB673E00448776 /* TDCChannelSpotlightSearchResultsTable.m */, ); path = "Channel Spotlight"; sourceTree = ""; }; 4C31527220EB673E00448776 /* License Manager */ = { isa = PBXGroup; children = ( 4C31527320EB673E00448776 /* Standalone */, ); path = "License Manager"; sourceTree = ""; }; 4C31527320EB673E00448776 /* Standalone */ = { isa = PBXGroup; children = ( 4C31527A20EB673E00448776 /* TDCLicenseManagerDialog.m */, 4C31527620EB673E00448776 /* TDCLicenseManagerMigrateAppStoreSheet.m */, 4C31527820EB673E00448776 /* TDCLicenseManagerRecoverLostLicenseSheet.m */, 4C31527920EB673E00448776 /* TDCLicenseUpgradeActivateSheet.m */, 4C31527720EB673E00448776 /* TDCLicenseUpgradeCommonActions.m */, 4C31527420EB673E00448776 /* TDCLicenseUpgradeDialog.m */, 4C31527520EB673E00448776 /* TDCLicenseUpgradeEligibilitySheet.m */, ); path = Standalone; sourceTree = ""; }; 4C31528D20EB673E00448776 /* Server Endpoint */ = { isa = PBXGroup; children = ( 4C31528F20EB673E00448776 /* TDCServerEndpointListSheet.m */, 4C31528E20EB673E00448776 /* TDCServerEndpointListSheetTable.m */, ); path = "Server Endpoint"; sourceTree = ""; }; 4C31529020EB673E00448776 /* File Transfers */ = { isa = PBXGroup; children = ( 4C31529120EB673E00448776 /* TDCFileTransferDialog.m */, 4C31529320EB673E00448776 /* TDCFileTransferDialogTableCell.m */, 4C31529220EB673E00448776 /* TDCFileTransferDialogTransferController.m */, ); path = "File Transfers"; sourceTree = ""; }; 4C31529A20EB673E00448776 /* Controllers */ = { isa = PBXGroup; children = ( 4C31529E20EB673E00448776 /* TXAppearance.m */, 4C31529C20EB673E00448776 /* TXApplication.m */, 4C31529B20EB673E00448776 /* TXGlobalModels.m */, 4C31529F20EB673E00448776 /* TXMasterController.m */, 4C3152A120EB673E00448776 /* TXMenuController.m */, 4C31529D20EB673E00448776 /* TXSharedApplication.m */, 4C3152A020EB673E00448776 /* TXWindowController.m */, ); path = Controllers; sourceTree = ""; }; 4C3152A220EB673E00448776 /* Views */ = { isa = PBXGroup; children = ( 4C3152AD20EB673E00448776 /* Channel Selection Table */, 4C3152B620EB673E00448776 /* Channel View */, 4C3152C620EB673E00448776 /* Errors */, 4C3152A420EB673E00448776 /* Input Text Field */, 4C3152CE20EB673E00448776 /* Main Window */, 4C3152AB20EB673E00448776 /* Preferences */, 4C3152CA20EB673E00448776 /* Server List */, 4C3152B020EB673E00448776 /* User List */, 4C3152C420EB673E00448776 /* TVCAppearance.m */, 4C3152A320EB673E00448776 /* TVCBasicTableView.m */, 4C3152B520EB673E00448776 /* TVCContentNavigationOutlineView.m */, 4C3152C520EB673E00448776 /* TVCDockIcon.m */, ); path = Views; sourceTree = ""; }; 4C3152A420EB673E00448776 /* Input Text Field */ = { isa = PBXGroup; children = ( 4C3152AA20EB673E00448776 /* TVCAutoExpandingTextField.m */, 4C3152A520EB673E00448776 /* TVCAutoExpandingTokenField.m */, 4C3152A620EB673E00448776 /* TVCTextFormatterMenu.m */, 4C3152A720EB673E00448776 /* TVCTextViewWithIRCFormatter.m */, 4C3152A820EB673E00448776 /* TVCValidatedComboBox.m */, 4C3152A920EB673E00448776 /* TVCValidatedTextField.m */, ); path = "Input Text Field"; sourceTree = ""; }; 4C3152AB20EB673E00448776 /* Preferences */ = { isa = PBXGroup; children = ( 4C3152AC20EB673E00448776 /* TVCNotificationConfigurationViewController.m */, ); path = Preferences; sourceTree = ""; }; 4C3152AD20EB673E00448776 /* Channel Selection Table */ = { isa = PBXGroup; children = ( 4C3152AE20EB673E00448776 /* TVCChannelSelectionOutlineCellView.m */, 4C3152AF20EB673E00448776 /* TVCChannelSelectionViewController.m */, ); path = "Channel Selection Table"; sourceTree = ""; }; 4C3152B020EB673E00448776 /* User List */ = { isa = PBXGroup; children = ( 4C3152B320EB673E00448776 /* TVCMemberList.m */, 4C3152B220EB673E00448776 /* TVCMemberListAppearance.m */, 4C3152B120EB673E00448776 /* TVCMemberListCell.m */, 4C3152B420EB673E00448776 /* TVCMemberListUserInfoPopover.m */, ); path = "User List"; sourceTree = ""; }; 4C3152B620EB673E00448776 /* Channel View */ = { isa = PBXGroup; children = ( 4C3152C020EB673E00448776 /* Extras */, 4C3152BA20EB673E00448776 /* TVCLogController.m */, 4C3152BE20EB673E00448776 /* TVCLogLine.m */, 4C3152B820EB673E00448776 /* TVCLogPolicy.m */, 4C3152BB20EB673E00448776 /* TVCLogRenderer.m */, 4C3152B720EB673E00448776 /* TVCLogScriptEventSink.m */, 4C3152BD20EB673E00448776 /* TVCLogView.m */, 4C3152BC20EB673E00448776 /* TVCLogViewInternalWK1.m */, 4C3152B920EB673E00448776 /* TVCLogViewInternalWK2.m */, 4C3152BF20EB673E00448776 /* TVCWK1AutoScroller.m */, ); path = "Channel View"; sourceTree = ""; }; 4C3152C020EB673E00448776 /* Extras */ = { isa = PBXGroup; children = ( 4C3152C220EB673E00448776 /* TVCLogControllerHistoricLogFile.m */, 4C3152C120EB673E00448776 /* TVCLogControllerInlineMediaService.m */, 4C3152C320EB673E00448776 /* TVCLogControllerOperationQueue.m */, ); path = Extras; sourceTree = ""; }; 4C3152C620EB673E00448776 /* Errors */ = { isa = PBXGroup; children = ( 4C3152C920EB673E00448776 /* TVCAlert.m */, 4C3152C720EB673E00448776 /* TVCErrorMessagePopover.m */, 4C3152C820EB673E00448776 /* TVCErrorMessagePopoverController.m */, ); path = Errors; sourceTree = ""; }; 4C3152CA20EB673E00448776 /* Server List */ = { isa = PBXGroup; children = ( 4C3152CC20EB673E00448776 /* TVCServerList.m */, 4C3152CD20EB673E00448776 /* TVCServerListAppearance.m */, 4C3152CB20EB673E00448776 /* TVCServerListCell.m */, ); path = "Server List"; sourceTree = ""; }; 4C3152CE20EB673E00448776 /* Main Window */ = { isa = PBXGroup; children = ( 4C3152D120EB673E00448776 /* TVCMainWindow.m */, 4C3152D520EB673E00448776 /* TVCMainWindowAppearance.m */, 4C3152D820EB673E00448776 /* TVCMainWindowChannelView.m */, 4C3152D220EB673E00448776 /* TVCMainWindowLoadingScreen.m */, 4C3152D420EB673E00448776 /* TVCMainWindowSegmentedControl.m */, 4C3152D320EB673E00448776 /* TVCMainWindowSplitView.m */, 4C3152D620EB673E00448776 /* TVCMainWindowTextView.m */, 4C3152CF20EB673E00448776 /* TVCMainWindowTextViewAppearance.m */, 4C3152D720EB673E00448776 /* TVCMainWindowTitlebarAccessoryView.m */, ); path = "Main Window"; sourceTree = ""; }; 4C3152D920EB673E00448776 /* Helpers */ = { isa = PBXGroup; children = ( 4C3152E420EB673E00448776 /* Cocoa (Objective-C) */, 4C3152DA20EB673E00448776 /* External Libraries */, 4C3152E020EB673E00448776 /* Plugin Architecture */, 4C3152DF20EB673E00448776 /* THOUnicodeHelper.m */, ); path = Helpers; sourceTree = ""; }; 4C3152DA20EB673E00448776 /* External Libraries */ = { isa = PBXGroup; children = ( 4C3152DD20EB673E00448776 /* Google */, 4C3152DB20EB673E00448776 /* WebKit */, ); path = "External Libraries"; sourceTree = ""; }; 4C3152DB20EB673E00448776 /* WebKit */ = { isa = PBXGroup; children = ( 4C3152DC20EB673E00448776 /* WebScriptObjectHelper.m */, ); path = WebKit; sourceTree = ""; }; 4C3152DD20EB673E00448776 /* Google */ = { isa = PBXGroup; children = ( 4C3152DE20EB673E00448776 /* GTMEncodeHTML.m */, ); path = Google; sourceTree = ""; }; 4C3152E020EB673E00448776 /* Plugin Architecture */ = { isa = PBXGroup; children = ( 4C3152E120EB673E00448776 /* THOPluginDispatcher.m */, 4C3152E320EB673E00448776 /* THOPluginItem.m */, 4C8F3FDB2C31B15200118AAF /* THOPluginItemLogging.m */, 4C3152E220EB673E00448776 /* THOPluginManager.m */, ); path = "Plugin Architecture"; sourceTree = ""; }; 4C3152E420EB673E00448776 /* Cocoa (Objective-C) */ = { isa = PBXGroup; children = ( 4C3152E520EB673E00448776 /* NSColorHelper.m */, 4C06E2A120EC4FA50055D09A /* NSObjectHelper.m */, 4C3152E720EB673E00448776 /* NSStringHelper.m */, 4C3152E820EB673E00448776 /* NSTableViewHelper.m */, 4C3152E620EB673E00448776 /* NSViewHelper.m */, 4C3152E920EB673E00448776 /* TXAppearanceHelper.m */, ); path = "Cocoa (Objective-C)"; sourceTree = ""; }; 4C3152EC20EB676400448776 /* Resources */ = { isa = PBXGroup; children = ( 4C3154B020EB67A400448776 /* Addons */, 4CCC6B46158E8C1B003A102B /* Build Settings */, 4C46CCA21580437400846B64 /* Documentation */, 4C31531720EB676400448776 /* Images */, 4C06E2A620EC4FF00055D09A /* Language Files */, 4C3152ED20EB676400448776 /* License Manager */, 4C31539B20EB676400448776 /* Property Lists */, 4C3152EF20EB676400448776 /* Scripting */, 4C3153A820EB676400448776 /* Styling */, 4C06E13620EC4AC40055D09A /* User Interface */, ); path = Resources; sourceTree = ""; }; 4C3152ED20EB676400448776 /* License Manager */ = { isa = PBXGroup; children = ( 4C3152EE20EB676400448776 /* RemoteLicenseSystemPublicKey.pub */, ); path = "License Manager"; sourceTree = ""; }; 4C3152EF20EB676400448776 /* Scripting */ = { isa = PBXGroup; children = ( 4C3152F020EB676400448776 /* Script Files */, ); path = Scripting; sourceTree = ""; }; 4C3152F020EB676400448776 /* Script Files */ = { isa = PBXGroup; children = ( 4C33341B20CC471A00ACB9AD /* Bundled Scripts */, ); path = "Script Files"; sourceTree = ""; }; 4C31531720EB676400448776 /* Images */ = { isa = PBXGroup; children = ( 4C31535720EB676400448776 /* Application */, 4C31535620EB676400448776 /* Copyright Information for Images.txt */, 4C31535C20EB676400448776 /* Dock Icon Badges */, 4C31534F20EB676400448776 /* Encryption Badges */, 4C31533C20EB676400448776 /* IRC Formatting Colors */, 4C31531820EB676400448776 /* Preferences */, 4C31532920EB676400448776 /* Status Badges */, 4C31537720EB676400448776 /* User Interface */, ); path = Images; sourceTree = ""; }; 4C31531820EB676400448776 /* Preferences */ = { isa = PBXGroup; children = ( 4C31531C20EB676400448776 /* TPWTB_Addons.png */, 4C31532120EB676400448776 /* TPWTB_Addons@2x.png */, 4C31531D20EB676400448776 /* TPWTB_Advanced.png */, 4C31531920EB676400448776 /* TPWTB_Advanced@2x.png */, 4C31531E20EB676400448776 /* TPWTB_Controls.png */, 4C31532320EB676400448776 /* TPWTB_Controls@2x.png */, 4C31532820EB676400448776 /* TPWTB_General.png */, 4C31532020EB676400448776 /* TPWTB_General@2x.png */, 4C31531A20EB676400448776 /* TPWTB_Highlights.png */, 4C31531B20EB676400448776 /* TPWTB_Highlights@2x.png */, 4C31532420EB676400448776 /* TPWTB_Interface.png */, 4C31532520EB676400448776 /* TPWTB_Interface@2x.png */, 4C31532620EB676400448776 /* TPWTB_Notifications.png */, 4C31532220EB676400448776 /* TPWTB_Notifications@2x.png */, 4C31532720EB676400448776 /* TPWTB_Style.png */, 4C31531F20EB676400448776 /* TPWTB_Style@2x.png */, ); path = Preferences; sourceTree = ""; }; 4C31532920EB676400448776 /* Status Badges */ = { isa = PBXGroup; children = ( 4C31533620EB676400448776 /* channelRoomStatusIconDarkActive.tif */, 4C31533220EB676400448776 /* channelRoomStatusIconDarkActive@2x.tif */, 4C31533920EB676400448776 /* channelRoomStatusIconDarkInactive.tif */, 4C31533420EB676400448776 /* channelRoomStatusIconDarkInactive@2x.tif */, 4C31533720EB676400448776 /* channelRoomStatusIconLightActive.tif */, 4C31533120EB676400448776 /* channelRoomStatusIconLightActive@2x.tif */, 4C31533320EB676400448776 /* channelRoomStatusIconLightInactive.tif */, 4C31533B20EB676400448776 /* channelRoomStatusIconLightInactive@2x.tif */, ); path = "Status Badges"; sourceTree = ""; }; 4C31533C20EB676400448776 /* IRC Formatting Colors */ = { isa = PBXGroup; children = ( 4C31534C20EB676400448776 /* FormattingColor_0.png */, 4C31534A20EB676400448776 /* FormattingColor_1.png */, 4C31534D20EB676400448776 /* FormattingColor_2.png */, 4C31534E20EB676400448776 /* FormattingColor_3.png */, 4C31534820EB676400448776 /* FormattingColor_4.png */, 4C31534920EB676400448776 /* FormattingColor_5.png */, 4C31534720EB676400448776 /* FormattingColor_6.png */, 4C31534620EB676400448776 /* FormattingColor_7.png */, 4C31533D20EB676400448776 /* FormattingColor_8.png */, 4C31533E20EB676400448776 /* FormattingColor_9.png */, 4C31534020EB676400448776 /* FormattingColor_10.png */, 4C31533F20EB676400448776 /* FormattingColor_11.png */, 4C31534120EB676400448776 /* FormattingColor_12.png */, 4C31534320EB676400448776 /* FormattingColor_13.png */, 4C31534420EB676400448776 /* FormattingColor_14.png */, 4C31534520EB676400448776 /* FormattingColor_15.png */, 4C31534220EB676400448776 /* FormattingColor_Rainbow.tif */, 4C31534B20EB676400448776 /* FormattingColor_Rainbow@2x.tif */, ); path = "IRC Formatting Colors"; sourceTree = ""; }; 4C31534F20EB676400448776 /* Encryption Badges */ = { isa = PBXGroup; children = ( 4C31535120EB676400448776 /* encryptionLockIconDark.tif */, 4C31535220EB676400448776 /* encryptionLockIconDark@2x.tif */, 4C31535320EB676400448776 /* encryptionLockIconLight.tif */, 4C31535020EB676400448776 /* encryptionLockIconLight@2x.tif */, ); path = "Encryption Badges"; sourceTree = ""; }; 4C31535720EB676400448776 /* Application */ = { isa = PBXGroup; children = ( 4C31535920EB676400448776 /* applicationIcon.iconset */, 4C31535820EB676400448776 /* applicationIcon.icns */, 4C31535A20EB676400448776 /* applicationIconBirthday.icns */, ); path = Application; sourceTree = ""; }; 4C31535C20EB676400448776 /* Dock Icon Badges */ = { isa = PBXGroup; children = ( 4C31535D20EB676400448776 /* Green */, 4C31536E20EB676400448776 /* Red */, ); path = "Dock Icon Badges"; sourceTree = ""; }; 4C31535D20EB676400448776 /* Green */ = { isa = PBXGroup; children = ( 4C31536120EB676400448776 /* DIGreenBadgeCenter.png */, 4C31535F20EB676400448776 /* DIGreenBadgeLeft.png */, 4C31536020EB676400448776 /* DIGreenBadgeRight.png */, ); path = Green; sourceTree = ""; }; 4C31536E20EB676400448776 /* Red */ = { isa = PBXGroup; children = ( 4C31537220EB676400448776 /* DIRedBadgeCenter.png */, 4C31537120EB676400448776 /* DIRedBadgeLeft.png */, 4C31537020EB676400448776 /* DIRedBadgeRight.png */, ); path = Red; sourceTree = ""; }; 4C31537720EB676400448776 /* User Interface */ = { isa = PBXGroup; children = ( 4C31537820EB676400448776 /* MainWindowSegmentedControlUserTemplate.pdf */, 4C31537920EB676400448776 /* MainWindowSpotlightIconTemplate.pdf */, 4C31537A20EB676400448776 /* Miscellaneous */, 4C31537C20EB676400448776 /* Server List */, ); path = "User Interface"; sourceTree = ""; }; 4C31537A20EB676400448776 /* Miscellaneous */ = { isa = PBXGroup; children = ( 4C31537B20EB676400448776 /* ErroneousTextFieldValueIndicator.tif */, ); path = Miscellaneous; sourceTree = ""; }; 4C31537C20EB676400448776 /* Server List */ = { isa = PBXGroup; children = ( 4C31537D20EB676400448776 /* Glass */, ); path = "Server List"; sourceTree = ""; }; 4C31537D20EB676400448776 /* Glass */ = { isa = PBXGroup; children = ( 4C31538220EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconActive.tif */, 4C31537F20EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif */, 4C31538620EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconInactive.tif */, 4C31537E20EB676400448776 /* VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif */, 4C31538320EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconActive.tif */, 4C31538520EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif */, 4C31538420EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconInactive.tif */, 4C31538720EB676400448776 /* VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif */, ); path = Glass; sourceTree = ""; }; 4C31539B20EB676400448776 /* Property Lists */ = { isa = PBXGroup; children = ( 4C315EE62C36494B001A16B4 /* Preferences */, 4C3153A020EB676400448776 /* IRCCommandIndexLocalData.plist */, 4C31539F20EB676400448776 /* IRCCommandIndexRemoteData.plist */, 4C3153A220EB676400448776 /* IRCNetworks.plist */, 4C3153A420EB676400448776 /* StaticStore.plist */, 4C3153A120EB676400448776 /* TemplateLineTypes.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C3153A820EB676400448776 /* Styling */ = { isa = PBXGroup; children = ( 4C33342020CC475300ACB9AD /* Bundled Styles */, 4CCD93FC1CA7291300335381 /* JavaScript */, 4C52379215C18F6700414852 /* Style Default Templates */, ); path = Styling; sourceTree = ""; }; 4C3154B020EB67A400448776 /* Addons */ = { isa = PBXGroup; children = ( 4C46CCA6158043BB00846B64 /* Extensions */, 4C69E3571B65C266002782D4 /* Installers */, 4C9A933B1FA659D1003C3DB5 /* XPC Services */, ); name = Addons; sourceTree = ""; }; 4C3154B120EB683200448776 /* Configurations */ = { isa = PBXGroup; children = ( 4C3154B420EB685700448776 /* Build */, 4C3154E620EB68A900448776 /* Sandbox */, ); name = Configurations; path = ../../..; sourceTree = ""; }; 4C3154B420EB685700448776 /* Build */ = { isa = PBXGroup; children = ( 4C3154D320EB685700448776 /* Code Signing Identity.xcconfig */, 4C3154C820EB685700448776 /* Common */, 4C3154DD20EB685700448776 /* Debug */, 4C3154B520EB685700448776 /* Standard Release */, ); name = Build; path = Configurations/Build; sourceTree = ""; }; 4C3154B520EB685700448776 /* Standard Release */ = { isa = PBXGroup; children = ( 4C3154BD20EB685700448776 /* Enabled Features.xcconfig */, 4C3154B720EB685700448776 /* Textual App.xcconfig */, 4C3154B920EB685700448776 /* Textual Extensions.xcconfig */, 4C3154B820EB685700448776 /* Textual.xcconfig */, 4C3154BC20EB685700448776 /* XPC Service - ICL Extensions.xcconfig */, 4C3154BA20EB685700448776 /* XPC Service - ICL.xcconfig */, 4C3154BB20EB685700448776 /* XPC Services.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4C3154C820EB685700448776 /* Common */ = { isa = PBXGroup; children = ( 4C3154D220EB685700448776 /* Foundation Debug.xcconfig */, 4C3154CF20EB685700448776 /* Foundation.xcconfig */, 4C3154CC20EB685700448776 /* Preserve Symbols.xcconfig */, 4C3154CA20EB685700448776 /* Textual App.xcconfig */, 4C3154CD20EB685700448776 /* Textual Extensions.xcconfig */, 4C3154CB20EB685700448776 /* Textual.xcconfig */, 4C3154D120EB685700448776 /* XPC Service - ICL Extensions.xcconfig */, 4C3154CE20EB685700448776 /* XPC Service - ICL.xcconfig */, 4C3154D020EB685700448776 /* XPC Services.xcconfig */, ); path = Common; sourceTree = ""; }; 4C3154DD20EB685700448776 /* Debug */ = { isa = PBXGroup; children = ( 4C3154E520EB685700448776 /* Enabled Features.xcconfig */, 4C3154DF20EB685700448776 /* Textual App.xcconfig */, 4C3154E120EB685700448776 /* Textual Extensions.xcconfig */, 4C3154E020EB685700448776 /* Textual.xcconfig */, 4C3154E420EB685700448776 /* XPC Service - ICL Extensions.xcconfig */, 4C3154E220EB685700448776 /* XPC Service - ICL.xcconfig */, 4C3154E320EB685700448776 /* XPC Services.xcconfig */, ); path = Debug; sourceTree = ""; }; 4C3154E620EB68A900448776 /* Sandbox */ = { isa = PBXGroup; children = ( 4C3154E820EB68A900448776 /* Debug.entitlements */, 4C3154E720EB68A900448776 /* Standard Release.entitlements */, ); name = Sandbox; path = Configurations/Sandbox; sourceTree = SOURCE_ROOT; }; 4C315EE62C36494B001A16B4 /* Preferences */ = { isa = PBXGroup; children = ( 4C2BEB632C3864C1008F8F0D /* KeysExcludedFromContainer.plist */, 4C315EE72C36581A001A16B4 /* KeysExcludedFromExport.plist */, 4C0A31122C37419D0081EF1F /* KeysExcludedFromMigrate.plist */, 4C315EE82C36581A001A16B4 /* PreferenceKeyMasterList.plist */, 4C3153A520EB676400448776 /* RegisteredUserDefaults.plist */, 4C3153A620EB676400448776 /* RegisteredUserDefaultsInContainer.plist */, ); path = Preferences; sourceTree = ""; }; 4C46CCA21580437400846B64 /* Documentation */ = { isa = PBXGroup; children = ( 4C58B570186011AC00834882 /* Acknowledgements.pdf */, ); name = Documentation; path = ..; sourceTree = ""; }; 4C46CCA6158043BB00846B64 /* Extensions */ = { isa = PBXGroup; children = ( 4C46CCA7158043C000846B64 /* Binaries */, ); name = Extensions; sourceTree = ""; }; 4C46CCA7158043C000846B64 /* Binaries */ = { isa = PBXGroup; children = ( 4C2E92202C1E4463003BB92D /* Caffeine.bundle */, 4C83B9101BA9BEB800BD7718 /* Chat Filters.bundle */, 4C956893194A8862003D97FD /* Smiley Converter.bundle */, 4C9A1D931836E6A100C14835 /* System Info.bundle */, 4C9A1D921836E6A100C14835 /* User Insights.bundle */, 4C9A1D941836E6A100C14835 /* ZNC Additions.bundle */, ); name = Binaries; sourceTree = ""; }; 4C46CCB91580469200846B64 /* System Frameworks */ = { isa = PBXGroup; children = ( 4CCE521A18C7BE0600D49601 /* AudioToolbox.framework */, 4C46CCBC1580469E00846B64 /* Cocoa.framework */, 4CA8EC621B63C2970087BF72 /* CoreServices.framework */, 4C10C00D1F37E59B0004C624 /* IOKit.framework */, 4CF4D6B619837C1700FB5AF0 /* QuartzCore.framework */, 4C46CCC01580469E00846B64 /* Security.framework */, 4C46CCC11580469E00846B64 /* SystemConfiguration.framework */, 4C2F42042C3F4218000D962D /* UserNotifications.framework */, 4C46CCC21580469E00846B64 /* WebKit.framework */, ); name = "System Frameworks"; sourceTree = ""; }; 4C46CCCC158046BE00846B64 /* External Frameworks */ = { isa = PBXGroup; children = ( 4CF76A371A91153A0088BF9A /* AutoHyperlinks.framework */, 4CF76A391A91153A0088BF9A /* CocoaExtensions.framework */, 4C8F2F5D1AAE6467007821CC /* EncryptionKit.framework */, 4CC4A7371F8A1404008FF15F /* GRMustache.framework */, 4C24DC751A93C9CE0098D1BE /* Sparkle.framework */, ); name = "External Frameworks"; sourceTree = ""; }; 4C4A0D6B1C973E8100064A23 /* Application Properties */ = { isa = PBXGroup; children = ( 4C4A0D6C1C973E9000064A23 /* Info.plist */, ); name = "Application Properties"; path = "Resources/Property Lists"; sourceTree = SOURCE_ROOT; }; 4C69E3571B65C266002782D4 /* Installers */ = { isa = PBXGroup; children = ( 4C2F0C701F2AF3860017D3B7 /* Textual-Extras.pkg */, ); name = Installers; sourceTree = ""; }; 4C9A933B1FA659D1003C3DB5 /* XPC Services */ = { isa = PBXGroup; children = ( 4C9A933C1FA659D9003C3DB5 /* Binaries */, ); name = "XPC Services"; sourceTree = ""; }; 4C9A933C1FA659D9003C3DB5 /* Binaries */ = { isa = PBXGroup; children = ( 4C171669254A589D004BE8AE /* Inline Content Loader.xpc */, 4C171668254A589D004BE8AE /* IRC Connection Host.xpc */, 4C17166A254A589E004BE8AE /* Scrollback History Manager.xpc */, ); name = Binaries; sourceTree = ""; }; 4CCC6B46158E8C1B003A102B /* Build Settings */ = { isa = PBXGroup; children = ( 4C4A0D6B1C973E8100064A23 /* Application Properties */, 5D9B8EB1170A0EA700919CB0 /* Build Scripts */, 4C3154B120EB683200448776 /* Configurations */, 4CB638CB2D45A06C002B2CC3 /* FeatureFlags.h */, 4CC70202171E1CB400BDFAE0 /* BuildConfig.h */, ); name = "Build Settings"; sourceTree = ""; }; 5D9B8EB1170A0EA700919CB0 /* Build Scripts */ = { isa = PBXGroup; children = ( 5D9B8EB9170A10F200919CB0 /* BuildExtensions.sh */, 5D9B8EB6170A0EB400919CB0 /* BuildFrameworks.sh */, 4C761BAE1E155CB100A505B7 /* BuildServices.sh */, 4CD6F7BB1CA7747600842597 /* ExportArchive.sh */, 4C48B46320E4D06900CF5F84 /* MergeSwift.sh */, 4C7961C6267D25B400D37F07 /* UpdateFeatureFlags.sh */, 5D9B8EB8170A0EB400919CB0 /* UpdateVersionInfo.sh */, 4C2BEB622C385189008F8F0D /* PostprocessSparkle.sh */, ); name = "Build Scripts"; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 4C5BA3D916F1302F00A96CA2 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 4CB638CC2D45A06C002B2CC3 /* FeatureFlags.h in Headers */, 4C3157BF20EB6D0600448776 /* TDCServerPropertiesSheetPrivate.h in Headers */, 4C3157A820EB6D0600448776 /* TDCFileTransferDialogTableCellPrivate.h in Headers */, 4CAB275F2537F71B009B1F07 /* IRCChannelMemberList.h in Headers */, 4C31578A20EB6D0600448776 /* IRCServerPrivate.h in Headers */, 4C3158BD20EB6D0F00448776 /* IRCHighlightLogEntryInternal.h in Headers */, 4C3157F920EB6D0600448776 /* TVCMemberListPrivate.h in Headers */, 4C31556520EB6CB300448776 /* IRCWorld.h in Headers */, 4C31577A20EB6D0600448776 /* IRCAddressBookUserTrackingPrivate.h in Headers */, 4C3158AF20EB6D0F00448776 /* IRCUserInternal.h in Headers */, 4C3157B020EB6D0600448776 /* TDCLicenseManagerMigrateAppStoreSheetPrivate.h in Headers */, 4C06E3F920EC51380055D09A /* ICLInlineContentProtocol.h in Headers */, 4C3157C020EB6D0600448776 /* TDCSharedProtocolDefinitionsPrivate.h in Headers */, 4C31555820EB6CB300448776 /* IRCHighlightLogEntry.h in Headers */, 4C06E26C20EC4C560055D09A /* NSObjectHelperPrivate.h in Headers */, 4C06E28120EC4E870055D09A /* TPCPreferences.h in Headers */, 4C3158BA20EB6D0F00448776 /* IRCMessageInternal.h in Headers */, 4C31559F20EB6CB300448776 /* TXGlobalModels.h in Headers */, 4C3157FB20EB6D0600448776 /* TVCNotificationConfigurationViewControllerPrivate.h in Headers */, 4C31559620EB6CB300448776 /* TVCMemberList.h in Headers */, 4C31558320EB6CB300448776 /* TPCTheme.h in Headers */, 4C3157F120EB6D0600448776 /* TVCMainWindowSegmentedControlPrivate.h in Headers */, 4C31556B20EB6CB300448776 /* TDCSheetBase.h in Headers */, 4C31559120EB6CB300448776 /* TVCMainWindowAppearance.h in Headers */, 4C3157A620EB6D0600448776 /* TDCChannelSpotlightSearchResultsTablePrivate.h in Headers */, 4C31579E20EB6D0600448776 /* TDCChannelModifyModesSheetPrivate.h in Headers */, 4C06E3FD20EC51380055D09A /* HLSHistoricLogProtocol.h in Headers */, 4C3157CC20EB6D0600448776 /* TLOLicenseManagerDownloaderPrivate.h in Headers */, 4C31578F20EB6D0600448776 /* IRCUserPrivate.h in Headers */, 4C31580120EB6D0600448776 /* TVCWK1AutoScrollerPrivate.h in Headers */, 4C3157A420EB6D0600448776 /* TDCChannelSpotlightControlsPrivate.h in Headers */, 4C31559B20EB6CB300448776 /* TVCValidatedComboBox.h in Headers */, 4C31556C20EB6CB300448776 /* TDCWindowBase.h in Headers */, 4C06E3B520EC51380055D09A /* GCDAsyncSocket.h in Headers */, 4C3157C120EB6D0600448776 /* TDCWelcomeSheetPrivate.h in Headers */, 4C3157C220EB6D0600448776 /* TextualPrivate.h in Headers */, 4C3158B920EB6D0F00448776 /* IRCModeInfoInternal.h in Headers */, 4C31578220EB6D0600448776 /* IRCColorFormatPrivate.h in Headers */, 4C3157B120EB6D0600448776 /* TDCLicenseManagerRecoverLostLicenseSheetPrivate.h in Headers */, 4C06E3B120EC51380055D09A /* GCDAsyncSocketExtensions.h in Headers */, 4C3157D320EB6D0600448776 /* TPCApplicationInfoPrivate.h in Headers */, 4C31580220EB6D0600448776 /* TXAppearancePrivate.h in Headers */, 4C31555620EB6CB300448776 /* IRCCommandIndex.h in Headers */, 4C31579620EB6D0600448776 /* TDCAboutDialogPrivate.h in Headers */, 4C3157E720EB6D0600448776 /* TVCLogLinePrivate.h in Headers */, 4C3157A220EB6D0600448776 /* TDCChannelSpotlightAppearancePrivate.h in Headers */, 4C3157E620EB6D0600448776 /* TVCLogControllerPrivate.h in Headers */, 4C3157D720EB6D0600448776 /* TPCPreferencesImportExportPrivate.h in Headers */, 4C3157B720EB6D0600448776 /* TDCPreferencesControllerPrivate.h in Headers */, 4C3157AF20EB6D0600448776 /* TDCLicenseManagerDialogPrivate.h in Headers */, 4C31578820EB6D0600448776 /* IRCMessageBatchPrivate.h in Headers */, 4C3157CD20EB6D0600448776 /* TLOLicenseManagerLastGenPrivate.h in Headers */, 4C31556E20EB6CB300448776 /* TextualApplication.h in Headers */, 4C31559020EB6CB300448776 /* TVCMainWindow.h in Headers */, 4C31559420EB6CB300448776 /* TVCMainWindowTextView.h in Headers */, 4C06E26420EC4C560055D09A /* TPCPreferencesPrivate.h in Headers */, 4C3158BC20EB6D0F00448776 /* TDCChannelSpotlightControllerInternal.h in Headers */, 4C06E25D20EC4BBD0055D09A /* IRCConnectionConfigInternal.h in Headers */, 4C31559520EB6CB300448776 /* TVCMainWindowTextViewAppearance.h in Headers */, 4C3157DB20EB6D0600448776 /* TPCResourceManagerPrivate.h in Headers */, 4C06E41220EC52240055D09A /* TVCLogLineXPCPrivate.h in Headers */, 4C3157A020EB6D0600448776 /* TDCChannelPropertiesNotificationConfigurationPrivate.h in Headers */, 4C31556320EB6CB300448776 /* IRCUser.h in Headers */, 4C31556F20EB6CB300448776 /* THOPluginProtocol.h in Headers */, 4C3157AA20EB6D0600448776 /* TDCHighlightEntrySheetPrivate.h in Headers */, 4C3157E920EB6D0600448776 /* TVCLogScriptEventSinkPrivate.h in Headers */, 4C3158B420EB6D0F00448776 /* IRCPrefixInternal.h in Headers */, 4C06E3C920EC51380055D09A /* ICLPayloadInternal.h in Headers */, 4C3157EE20EB6D0600448776 /* TVCMainWindowChannelViewPrivate.h in Headers */, 4C3157C520EB6D0600448776 /* THOPluginManagerPrivate.h in Headers */, 4C31558020EB6CB300448776 /* TPCPreferencesUserDefaultsLocal.h in Headers */, 4C3157D820EB6D0600448776 /* TPCPreferencesLocalPrivate.h in Headers */, 4C3157EB20EB6D0600448776 /* TVCLogViewInternalWK2.h in Headers */, 4C31555B20EB6CB300448776 /* IRCMessage.h in Headers */, 4C31578D20EB6D0600448776 /* IRCUserNicknameColorStyleGeneratorPrivate.h in Headers */, 4C3157B820EB6D0600448776 /* TDCPreferencesNotificationConfigurationPrivate.h in Headers */, 4C31580620EB6D0600448776 /* TXMenuControllerPrivate.h in Headers */, 4C8878082C3371470016DB98 /* TPCSandboxMigrationPrivate.h in Headers */, 4C31558920EB6CB300448776 /* TVCChannelSelectionViewController.h in Headers */, 4C31577C20EB6D0600448776 /* IRCChannelModePrivate.h in Headers */, 4C3157F420EB6D0600448776 /* TVCMainWindowTextViewAppearancePrivate.h in Headers */, 4C31556920EB6CB300448776 /* TDCAlert.h in Headers */, 4C31555320EB6CB300448776 /* IRCClient.h in Headers */, 4C3157FD20EB6D0600448776 /* TVCServerListCellPrivate.h in Headers */, 4C3158B120EB6D0F00448776 /* TDCChannelPropertiesSheetInternal.h in Headers */, 4C31578E20EB6D0600448776 /* IRCUserPersistentStorePrivate.h in Headers */, 4C31559820EB6CB300448776 /* TVCServerList.h in Headers */, 4C3157B320EB6D0600448776 /* TDCLicenseUpgradeCommonActionsPrivate.h in Headers */, 4C31558B20EB6CB300448776 /* TVCErrorMessagePopoverPrivate.h in Headers */, 4C3157FE20EB6D0600448776 /* TVCServerListPrivate.h in Headers */, 4C3157CE20EB6D0600448776 /* TLOLicenseManagerPrivate.h in Headers */, 4C31579320EB6D0600448776 /* NSTableVIewHelperPrivate.h in Headers */, 4C31578520EB6D0600448776 /* IRCExtrasPrivate.h in Headers */, 4C31557320EB6CB300448776 /* TLOInternetAddressLookup.h in Headers */, 4C31556D20EB6CB300448776 /* Textual.h in Headers */, 4C31555720EB6CB300448776 /* IRCConnection.h in Headers */, 4C31578C20EB6D0600448776 /* IRCTreeItemPrivate.h in Headers */, 4C3157BD20EB6D0600448776 /* TDCServerEndpointListSheetTablePrivate.h in Headers */, 4C3158B820EB6D0F00448776 /* IRCChannelUserInternal.h in Headers */, 4C3155A220EB6CB300448776 /* TXSharedApplication.h in Headers */, 4C7B05001D1210F300295E82 /* BuildConfig.h in Headers */, 4C31577E20EB6D0600448776 /* IRCChannelUserPrivate.h in Headers */, 4C31579420EB6D0600448776 /* NSViewHelperPrivate.h in Headers */, 4C3157B220EB6D0600448776 /* TDCLicenseUpgradeActivateSheetPrivate.h in Headers */, 4C3158BE20EB6D0F00448776 /* IRCHighlightMatchConditionInternal.h in Headers */, 4C31558D20EB6CB300448776 /* TVCLogLine.h in Headers */, 4C3158B220EB6D0F00448776 /* TDCChannelSpotlightAppearanceInternal.h in Headers */, 4C31580720EB6D0600448776 /* TXSharedApplicationPrivate.h in Headers */, 4C31578B20EB6D0600448776 /* IRCTimerCommandPrivate.h in Headers */, 4C3157CB20EB6D0600448776 /* TLOInputHistoryPrivate.h in Headers */, 4C3157D220EB6D0600448776 /* TLOSpokenNotificationPrivate.h in Headers */, 4C31558820EB6CB300448776 /* TVCBasicTableView.h in Headers */, 4C31558220EB6CB300448776 /* TPCThemeController.h in Headers */, 4C31556720EB6CB300448776 /* NSStringHelper.h in Headers */, 4C3157DC20EB6D0600448776 /* TPCThemeControllerPrivate.h in Headers */, 4C3157C620EB6D0600448776 /* THOPluginProtocolPrivate.h in Headers */, 4C3157E220EB6D0600448776 /* TVCDockIconPrivate.h in Headers */, 4C31558720EB6CB300448776 /* TVCAutoExpandingTokenField.h in Headers */, 4C31577B20EB6D0600448776 /* IRCChannelConfigPrivate.h in Headers */, 4C31558C20EB6CB300448776 /* TVCLogController.h in Headers */, 4C3158AE20EB6D0F00448776 /* IRCServerInternal.h in Headers */, 4C31555C20EB6CB300448776 /* IRCModeInfo.h in Headers */, 4C31555D20EB6CB300448776 /* IRCNetworkList.h in Headers */, 4C31578420EB6D0600448776 /* IRCConnectionPrivate.h in Headers */, 4C31558420EB6CB300448776 /* TVCAlert.h in Headers */, 4C3157C420EB6D0600448776 /* THOPluginItemPrivate.h in Headers */, 4C31577920EB6D0600448776 /* IRCAddressBookMatchCachePrivate.h in Headers */, 4C31557D20EB6CB300448776 /* TPCPreferencesImportExport.h in Headers */, 4C31556020EB6CB300448776 /* IRCSendingMessage.h in Headers */, 4C31557A20EB6CB300448776 /* TPCPathInfo.h in Headers */, 4C31554F20EB6CB300448776 /* IRCChannel.h in Headers */, 4C31557420EB6CB300448776 /* TLOKeyEventHandler.h in Headers */, 4C3158E620EB6D1600448776 /* OELReachability.h in Headers */, 4C31555520EB6CB300448776 /* IRCColorFormat.h in Headers */, 4C3158B620EB6D0F00448776 /* IRCClientConfigInternal.h in Headers */, 4C3155A120EB6CB300448776 /* TXMenuController.h in Headers */, 4C31555A20EB6CB300448776 /* IRCISupportInfo.h in Headers */, 4C31580820EB6D0600448776 /* TXWindowControllerPrivate.h in Headers */, 4C3157D420EB6D0600448776 /* TPCPathInfoPrivate.h in Headers */, 4CEFEDAC22B4AF42002CEE19 /* TDCPreferencesUserStyleSheetPrivate.h in Headers */, 4C31559720EB6CB300448776 /* TVCMemberListAppearance.h in Headers */, 4C3158E520EB6D1600448776 /* GTMEncodeHTML.h in Headers */, 4C3157BE20EB6D0600448776 /* TDCServerHighlightListSheetPrivate.h in Headers */, 4C06E27120EC4E350055D09A /* StaticDefinitions.h in Headers */, 4C31580420EB6D0600448776 /* TXGlobalModelsPrivate.h in Headers */, 4C31580920EB6D0600448776 /* WKWebViewPrivate.h in Headers */, 4C3157A920EB6D0600448776 /* TDCFileTransferDialogTransferControllerPrivate.h in Headers */, 4C3157EA20EB6D0600448776 /* TVCLogViewInternalWK1.h in Headers */, 4C31557820EB6CB300448776 /* TLOSoundPlayer.h in Headers */, 4C31557920EB6CB300448776 /* TPCApplicationInfo.h in Headers */, 4C3157A720EB6D0600448776 /* TDCFileTransferDialogPrivate.h in Headers */, 4C31578320EB6D0600448776 /* IRCCommandIndexPrivate.h in Headers */, 4C31559D20EB6CB300448776 /* TXAppearance.h in Headers */, 4C3157DD20EB6D0600448776 /* TPCThemePrivate.h in Headers */, 4C31554D20EB6CB300448776 /* IRCAddressBook.h in Headers */, 4C3157D120EB6D0600448776 /* TLOSpeechSynthesizerPrivate.h in Headers */, 4C06E24920EC4B970055D09A /* IRCConnectionConfig.h in Headers */, 4C31557520EB6CB300448776 /* TLOLinkParser.h in Headers */, 4C06E3F520EC51380055D09A /* RCMConnectionManagerProtocol.h in Headers */, 4C31577F20EB6D0600448776 /* IRCClientConfigPrivate.h in Headers */, 4C31580520EB6D0600448776 /* TXMasterControllerPrivate.h in Headers */, 4C3157F320EB6D0600448776 /* TVCMainWindowSplitViewPrivate.h in Headers */, 4C3157FA20EB6D0600448776 /* TVCMemberListUserInfoPopoverPrivate.h in Headers */, 4C31579F20EB6D0600448776 /* TDCChannelModifyTopicSheetPrivate.h in Headers */, 4C31579120EB6D0600448776 /* IRCWorldPrivate.h in Headers */, 4C31559A20EB6CB300448776 /* TVCTextViewWithIRCFormatter.h in Headers */, 4C31580020EB6D0600448776 /* TVCTextViewWithIRCFormatterPrivate.h in Headers */, 4C3157EC20EB6D0600448776 /* TVCLogViewPrivate.h in Headers */, 4C3157FF20EB6D0600448776 /* TVCTextFormatterMenuPrivate.h in Headers */, 4C31556820EB6CB300448776 /* NSViewHelper.h in Headers */, 4C3157F520EB6D0600448776 /* TVCMainWindowTextViewPrivate.h in Headers */, 4C3157B920EB6D0600448776 /* TDCProgressIndicatorSheetPrivate.h in Headers */, 4C06E3D520EC51380055D09A /* ICLPayload.h in Headers */, 4C8F3FD92C31AF7700118AAF /* THOPluginManager.h in Headers */, 4C3157E420EB6D0600448776 /* TVCLogControllerInlineMediaServicePrivate.h in Headers */, 4C3157B620EB6D0600448776 /* TDCNicknameColorSheetPrivate.h in Headers */, 4C31578620EB6D0600448776 /* IRCHighlightLogEntryPrivate.h in Headers */, 4C3158B520EB6D0F00448776 /* IRCChannelConfigInternal.h in Headers */, 4C31558A20EB6CB300448776 /* TVCErrorMessagePopoverControllerPrivate.h in Headers */, 4C3157A520EB6D0600448776 /* TDCChannelSpotlightSearchResultPrivate.h in Headers */, 4C3157C820EB6D0600448776 /* TLOEncryptionManagerPrivate.h in Headers */, 4C31577720EB6D0600448776 /* WebScriptObjectHelperPrivate.h in Headers */, 4C3157F720EB6D0600448776 /* TVCMemberListAppearancePrivate.h in Headers */, 4C31558120EB6CB300448776 /* TPCResourceManager.h in Headers */, 4C3157D020EB6D0600448776 /* TLONotificationConfigurationPrivate.h in Headers */, 4C31579D20EB6D0600448776 /* TDCChannelInviteSheetPrivate.h in Headers */, 4C31558F20EB6CB300448776 /* TVCLogView.h in Headers */, 4C31554C20EB6CB300448776 /* IRC.h in Headers */, 4C31556A20EB6CB300448776 /* TDCInputPrompt.h in Headers */, 4C3157B520EB6D0600448776 /* TDCLicenseUpgradeEligibilitySheetPrivate.h in Headers */, 4C3157ED20EB6D0600448776 /* TVCMainWindowAppearancePrivate.h in Headers */, 4C06E27D20EC4E870055D09A /* TPCPreferencesUserDefaults.h in Headers */, 4C31578720EB6D0600448776 /* IRCISupportInfoPrivate.h in Headers */, 4C5274E220F8D78800B18F9D /* IRCConnectionErrors.h in Headers */, 4C06E27920EC4E870055D09A /* TLOTimer.h in Headers */, 4C31555220EB6CB300448776 /* IRCChannelUser.h in Headers */, 4C3157FC20EB6D0600448776 /* TVCServerListAppearancePrivate.h in Headers */, 4C3157A320EB6D0600448776 /* TDCChannelSpotlightControllerPrivate.h in Headers */, 4C3157CF20EB6D0600448776 /* TLONicknameCompletionStatusPrivate.h in Headers */, 4C31578920EB6D0600448776 /* IRCMessagePrivate.h in Headers */, 4C31577D20EB6D0600448776 /* IRCChannelPrivate.h in Headers */, 4C3157C920EB6D0600448776 /* TLOFileLoggerPrivate.h in Headers */, 4C31555120EB6CB300448776 /* IRCChannelMode.h in Headers */, 4C3157E120EB6D0600448776 /* TVCContentNavigationOutlineViewPrivate.h in Headers */, 4C31556220EB6CB300448776 /* IRCTreeItem.h in Headers */, 4C06E26820EC4C560055D09A /* TPCPreferencesUserDefaultsPrivate.h in Headers */, 4C31559220EB6CB300448776 /* TVCMainWindowLoadingScreen.h in Headers */, 4C3158B720EB6D0F00448776 /* TVCLogLineInternal.h in Headers */, 4C31556420EB6CB300448776 /* IRCUserRelations.h in Headers */, 4C3157EF20EB6D0600448776 /* TVCMainWindowLoadingScreenPrivate.h in Headers */, 4C31556120EB6CB300448776 /* IRCServer.h in Headers */, 4C31579520EB6D0600448776 /* SwiftBridgingHeaderPrivate.h in Headers */, 4C31578120EB6D0600448776 /* IRCClientRequestedCommandsPrivate.h in Headers */, 4C31555E20EB6CB300448776 /* IRCNumerics.h in Headers */, 4C31555F20EB6CB300448776 /* IRCPrefix.h in Headers */, 4C3157E320EB6D0600448776 /* TVCLogControllerHistoricLogFilePrivate.h in Headers */, 4CAB276C253863D9009B1F07 /* IRCChannelMemberListControllerPrivate.h in Headers */, 4C06E41720EC52D60055D09A /* ICLPayloadLocalPrivate.h in Headers */, 4C31557F20EB6CB300448776 /* TPCPreferencesReload.h in Headers */, 4C31557220EB6CB300448776 /* TLONotificationController.h in Headers */, 4C3157BB20EB6D0600448776 /* TDCServerChannelListDialogPrivate.h in Headers */, 4C31554E20EB6CB300448776 /* IRCAddressBookUserTracking.h in Headers */, 4C3157CA20EB6D0600448776 /* TLONotificationControllerPrivate.h in Headers */, 4C3157BC20EB6D0600448776 /* TDCServerEndpointListSheetPrivate.h in Headers */, 4C3157F620EB6D0600448776 /* TVCMainWindowTitlebarAccessoryViewPrivate.h in Headers */, 4C31580320EB6D0600448776 /* TXApplicationPrivate.h in Headers */, 4C3157F020EB6D0600448776 /* TVCMainWindowPrivate.h in Headers */, 4C3158B020EB6D0F00448776 /* IRCAddressBookInternal.h in Headers */, 4C31559E20EB6CB300448776 /* TXAppearanceHelper.h in Headers */, 4C31555420EB6CB300448776 /* IRCClientConfig.h in Headers */, 4C3157DE20EB6D0600448776 /* TVCAppearancePrivate.h in Headers */, 4C3157C320EB6D0600448776 /* THOPluginDispatcherPrivate.h in Headers */, 4C31556620EB6CB300448776 /* NSColorHelper.h in Headers */, 4C31555920EB6CB300448776 /* IRCHighlightMatchCondition.h in Headers */, 4C31558620EB6CB300448776 /* TVCAutoExpandingTextField.h in Headers */, 4C31559920EB6CB300448776 /* TVCServerListAppearance.h in Headers */, 4C3157F820EB6D0600448776 /* TVCMemberListCellPrivate.h in Headers */, 4C31579020EB6D0600448776 /* IRCUserRelationsPrivate.h in Headers */, 4C3157E020EB6D0600448776 /* TVCChannelSelectionViewControllerPrivate.h in Headers */, 4C3157E820EB6D0600448776 /* TVCLogPolicyPrivate.h in Headers */, 4C31557020EB6CB300448776 /* THOUnicodeHelper.h in Headers */, 4C31558E20EB6CB300448776 /* TVCLogRenderer.h in Headers */, 4C31559C20EB6CB300448776 /* TVCValidatedTextField.h in Headers */, 4C31557120EB6CB300448776 /* TLOEncryptionManager.h in Headers */, 4C3155A020EB6CB300448776 /* TXMasterController.h in Headers */, 4C31579C20EB6D0600448776 /* TDCChannelBanListSheetPrivate.h in Headers */, 4C31578020EB6D0600448776 /* IRCClientPrivate.h in Headers */, 4C3157BA20EB6D0600448776 /* TDCServerChangeNicknameSheetPrivate.h in Headers */, 4C31557E20EB6CB300448776 /* TPCPreferencesLocal.h in Headers */, 4CAB27582537EE66009B1F07 /* IRCChannelMemberListPrivate.h in Headers */, 4C3157B420EB6D0600448776 /* TDCLicenseUpgradeDialogPrivate.h in Headers */, 4C31558520EB6CB300448776 /* TVCAppearance.h in Headers */, 4C31557620EB6CB300448776 /* TLOpenLink.h in Headers */, 4C06E28520EC4E870055D09A /* TLOLocalization.h in Headers */, 4C3158BB20EB6D0F00448776 /* TDCFileTransferDialogInternal.h in Headers */, 4C31555020EB6CB300448776 /* IRCChannelConfig.h in Headers */, 4C31559320EB6CB300448776 /* TVCMainWindowSplitView.h in Headers */, 4C3157DF20EB6D0600448776 /* TVCChannelSelectionOutlineViewCellPrivate.h in Headers */, 4C3157A120EB6D0600448776 /* TDCChannelPropertiesSheetPrivate.h in Headers */, 4C31579720EB6D0600448776 /* TDCAddressBookSheetPrivate.h in Headers */, 4C3157E520EB6D0600448776 /* TVCLogControllerOperationQueuePrivate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 4CCF2F9E15804C18006FFE21 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 4CB638CD2D45A06C002B2CC3 /* FeatureFlags.h in Headers */, 4C31572C20EB6D0500448776 /* TDCServerPropertiesSheetPrivate.h in Headers */, 4CAB27602537F71B009B1F07 /* IRCChannelMemberList.h in Headers */, 4C31571520EB6D0500448776 /* TDCFileTransferDialogTableCellPrivate.h in Headers */, 4C3156F720EB6D0500448776 /* IRCServerPrivate.h in Headers */, 4C3158CE20EB6D1000448776 /* IRCHighlightLogEntryInternal.h in Headers */, 4C31576620EB6D0500448776 /* TVCMemberListPrivate.h in Headers */, 4C3155BC20EB6CB400448776 /* IRCWorld.h in Headers */, 4C3156E720EB6D0500448776 /* IRCAddressBookUserTrackingPrivate.h in Headers */, 4C3158C020EB6D1000448776 /* IRCUserInternal.h in Headers */, 4C31571D20EB6D0500448776 /* TDCLicenseManagerMigrateAppStoreSheetPrivate.h in Headers */, 4C06E3FA20EC51380055D09A /* ICLInlineContentProtocol.h in Headers */, 4C31572D20EB6D0500448776 /* TDCSharedProtocolDefinitionsPrivate.h in Headers */, 4C3155AF20EB6CB400448776 /* IRCHighlightLogEntry.h in Headers */, 4C06E26D20EC4C560055D09A /* NSObjectHelperPrivate.h in Headers */, 4C06E28220EC4E870055D09A /* TPCPreferences.h in Headers */, 4C3158CB20EB6D1000448776 /* IRCMessageInternal.h in Headers */, 4C3155F620EB6CB400448776 /* TXGlobalModels.h in Headers */, 4C31576820EB6D0500448776 /* TVCNotificationConfigurationViewControllerPrivate.h in Headers */, 4C3155ED20EB6CB400448776 /* TVCMemberList.h in Headers */, 4C3155DA20EB6CB400448776 /* TPCTheme.h in Headers */, 4C31575E20EB6D0500448776 /* TVCMainWindowSegmentedControlPrivate.h in Headers */, 4C3155C220EB6CB400448776 /* TDCSheetBase.h in Headers */, 4C3155E820EB6CB400448776 /* TVCMainWindowAppearance.h in Headers */, 4C31571320EB6D0500448776 /* TDCChannelSpotlightSearchResultsTablePrivate.h in Headers */, 4C31570B20EB6D0500448776 /* TDCChannelModifyModesSheetPrivate.h in Headers */, 4C06E3FE20EC51380055D09A /* HLSHistoricLogProtocol.h in Headers */, 4C31573920EB6D0500448776 /* TLOLicenseManagerDownloaderPrivate.h in Headers */, 4C3156FC20EB6D0500448776 /* IRCUserPrivate.h in Headers */, 4C31576E20EB6D0500448776 /* TVCWK1AutoScrollerPrivate.h in Headers */, 4C31571120EB6D0500448776 /* TDCChannelSpotlightControlsPrivate.h in Headers */, 4C3155F220EB6CB400448776 /* TVCValidatedComboBox.h in Headers */, 4C3155C320EB6CB400448776 /* TDCWindowBase.h in Headers */, 4C06E3B620EC51380055D09A /* GCDAsyncSocket.h in Headers */, 4C31572E20EB6D0500448776 /* TDCWelcomeSheetPrivate.h in Headers */, 4C31572F20EB6D0500448776 /* TextualPrivate.h in Headers */, 4C3158CA20EB6D1000448776 /* IRCModeInfoInternal.h in Headers */, 4C3156EF20EB6D0500448776 /* IRCColorFormatPrivate.h in Headers */, 4C31571E20EB6D0500448776 /* TDCLicenseManagerRecoverLostLicenseSheetPrivate.h in Headers */, 4C06E3B220EC51380055D09A /* GCDAsyncSocketExtensions.h in Headers */, 4C31574020EB6D0500448776 /* TPCApplicationInfoPrivate.h in Headers */, 4C31576F20EB6D0500448776 /* TXAppearancePrivate.h in Headers */, 4C3155AD20EB6CB400448776 /* IRCCommandIndex.h in Headers */, 4C31570320EB6D0500448776 /* TDCAboutDialogPrivate.h in Headers */, 4C31575420EB6D0500448776 /* TVCLogLinePrivate.h in Headers */, 4C31570F20EB6D0500448776 /* TDCChannelSpotlightAppearancePrivate.h in Headers */, 4C31575320EB6D0500448776 /* TVCLogControllerPrivate.h in Headers */, 4C31574420EB6D0500448776 /* TPCPreferencesImportExportPrivate.h in Headers */, 4C31572420EB6D0500448776 /* TDCPreferencesControllerPrivate.h in Headers */, 4C31571C20EB6D0500448776 /* TDCLicenseManagerDialogPrivate.h in Headers */, 4C3156F520EB6D0500448776 /* IRCMessageBatchPrivate.h in Headers */, 4C31573A20EB6D0500448776 /* TLOLicenseManagerLastGenPrivate.h in Headers */, 4C3155C520EB6CB400448776 /* TextualApplication.h in Headers */, 4C3155E720EB6CB400448776 /* TVCMainWindow.h in Headers */, 4C3155EB20EB6CB400448776 /* TVCMainWindowTextView.h in Headers */, 4C06E26520EC4C560055D09A /* TPCPreferencesPrivate.h in Headers */, 4C3158CD20EB6D1000448776 /* TDCChannelSpotlightControllerInternal.h in Headers */, 4C06E25E20EC4BBD0055D09A /* IRCConnectionConfigInternal.h in Headers */, 4C3155EC20EB6CB400448776 /* TVCMainWindowTextViewAppearance.h in Headers */, 4C31574820EB6D0500448776 /* TPCResourceManagerPrivate.h in Headers */, 4C06E41320EC52240055D09A /* TVCLogLineXPCPrivate.h in Headers */, 4C31570D20EB6D0500448776 /* TDCChannelPropertiesNotificationConfigurationPrivate.h in Headers */, 4C3155BA20EB6CB400448776 /* IRCUser.h in Headers */, 4C3155C620EB6CB400448776 /* THOPluginProtocol.h in Headers */, 4C31571720EB6D0500448776 /* TDCHighlightEntrySheetPrivate.h in Headers */, 4C31575620EB6D0500448776 /* TVCLogScriptEventSinkPrivate.h in Headers */, 4C3158C520EB6D1000448776 /* IRCPrefixInternal.h in Headers */, 4C06E3CA20EC51380055D09A /* ICLPayloadInternal.h in Headers */, 4C31575B20EB6D0500448776 /* TVCMainWindowChannelViewPrivate.h in Headers */, 4C31573220EB6D0500448776 /* THOPluginManagerPrivate.h in Headers */, 4C3155D720EB6CB400448776 /* TPCPreferencesUserDefaultsLocal.h in Headers */, 4C31574520EB6D0500448776 /* TPCPreferencesLocalPrivate.h in Headers */, 4C31575820EB6D0500448776 /* TVCLogViewInternalWK2.h in Headers */, 4C3155B220EB6CB400448776 /* IRCMessage.h in Headers */, 4C3156FA20EB6D0500448776 /* IRCUserNicknameColorStyleGeneratorPrivate.h in Headers */, 4C31572520EB6D0500448776 /* TDCPreferencesNotificationConfigurationPrivate.h in Headers */, 4C31577320EB6D0500448776 /* TXMenuControllerPrivate.h in Headers */, 4C8878072C3371470016DB98 /* TPCSandboxMigrationPrivate.h in Headers */, 4C3155E020EB6CB400448776 /* TVCChannelSelectionViewController.h in Headers */, 4C3156E920EB6D0500448776 /* IRCChannelModePrivate.h in Headers */, 4C31576120EB6D0500448776 /* TVCMainWindowTextViewAppearancePrivate.h in Headers */, 4C3155C020EB6CB400448776 /* TDCAlert.h in Headers */, 4C3155AA20EB6CB400448776 /* IRCClient.h in Headers */, 4C31576A20EB6D0500448776 /* TVCServerListCellPrivate.h in Headers */, 4C3158C220EB6D1000448776 /* TDCChannelPropertiesSheetInternal.h in Headers */, 4C3156FB20EB6D0500448776 /* IRCUserPersistentStorePrivate.h in Headers */, 4C3155EF20EB6CB400448776 /* TVCServerList.h in Headers */, 4C31572020EB6D0500448776 /* TDCLicenseUpgradeCommonActionsPrivate.h in Headers */, 4C3155E220EB6CB400448776 /* TVCErrorMessagePopoverPrivate.h in Headers */, 4C31576B20EB6D0500448776 /* TVCServerListPrivate.h in Headers */, 4C31573B20EB6D0500448776 /* TLOLicenseManagerPrivate.h in Headers */, 4C31570020EB6D0500448776 /* NSTableVIewHelperPrivate.h in Headers */, 4C3156F220EB6D0500448776 /* IRCExtrasPrivate.h in Headers */, 4C3155CA20EB6CB400448776 /* TLOInternetAddressLookup.h in Headers */, 4C3155C420EB6CB400448776 /* Textual.h in Headers */, 4C3155AE20EB6CB400448776 /* IRCConnection.h in Headers */, 4C3156F920EB6D0500448776 /* IRCTreeItemPrivate.h in Headers */, 4C31572A20EB6D0500448776 /* TDCServerEndpointListSheetTablePrivate.h in Headers */, 4C3158C920EB6D1000448776 /* IRCChannelUserInternal.h in Headers */, 4C3155F920EB6CB400448776 /* TXSharedApplication.h in Headers */, 4C7B05011D12110500295E82 /* BuildConfig.h in Headers */, 4C3156EB20EB6D0500448776 /* IRCChannelUserPrivate.h in Headers */, 4C31570120EB6D0500448776 /* NSViewHelperPrivate.h in Headers */, 4C31571F20EB6D0500448776 /* TDCLicenseUpgradeActivateSheetPrivate.h in Headers */, 4C3158CF20EB6D1000448776 /* IRCHighlightMatchConditionInternal.h in Headers */, 4C3155E420EB6CB400448776 /* TVCLogLine.h in Headers */, 4C3158C320EB6D1000448776 /* TDCChannelSpotlightAppearanceInternal.h in Headers */, 4C31577420EB6D0500448776 /* TXSharedApplicationPrivate.h in Headers */, 4C3156F820EB6D0500448776 /* IRCTimerCommandPrivate.h in Headers */, 4C31573820EB6D0500448776 /* TLOInputHistoryPrivate.h in Headers */, 4C31573F20EB6D0500448776 /* TLOSpokenNotificationPrivate.h in Headers */, 4C3155DF20EB6CB400448776 /* TVCBasicTableView.h in Headers */, 4C3155D920EB6CB400448776 /* TPCThemeController.h in Headers */, 4C3155BE20EB6CB400448776 /* NSStringHelper.h in Headers */, 4C31574920EB6D0500448776 /* TPCThemeControllerPrivate.h in Headers */, 4C31573320EB6D0500448776 /* THOPluginProtocolPrivate.h in Headers */, 4C31574F20EB6D0500448776 /* TVCDockIconPrivate.h in Headers */, 4C3155DE20EB6CB400448776 /* TVCAutoExpandingTokenField.h in Headers */, 4C3156E820EB6D0500448776 /* IRCChannelConfigPrivate.h in Headers */, 4C3155E320EB6CB400448776 /* TVCLogController.h in Headers */, 4C3158BF20EB6D1000448776 /* IRCServerInternal.h in Headers */, 4C3155B320EB6CB400448776 /* IRCModeInfo.h in Headers */, 4C3155B420EB6CB400448776 /* IRCNetworkList.h in Headers */, 4C3156F120EB6D0500448776 /* IRCConnectionPrivate.h in Headers */, 4C3155DB20EB6CB400448776 /* TVCAlert.h in Headers */, 4C31573120EB6D0500448776 /* THOPluginItemPrivate.h in Headers */, 4C3156E620EB6D0500448776 /* IRCAddressBookMatchCachePrivate.h in Headers */, 4C3155D420EB6CB400448776 /* TPCPreferencesImportExport.h in Headers */, 4C3155B720EB6CB400448776 /* IRCSendingMessage.h in Headers */, 4C3155D120EB6CB400448776 /* TPCPathInfo.h in Headers */, 4C3155A620EB6CB400448776 /* IRCChannel.h in Headers */, 4C3155CB20EB6CB400448776 /* TLOKeyEventHandler.h in Headers */, 4C3158E420EB6D1600448776 /* OELReachability.h in Headers */, 4C3155AC20EB6CB400448776 /* IRCColorFormat.h in Headers */, 4C3158C720EB6D1000448776 /* IRCClientConfigInternal.h in Headers */, 4C3155F820EB6CB400448776 /* TXMenuController.h in Headers */, 4C3155B120EB6CB400448776 /* IRCISupportInfo.h in Headers */, 4C31577520EB6D0500448776 /* TXWindowControllerPrivate.h in Headers */, 4C31574120EB6D0500448776 /* TPCPathInfoPrivate.h in Headers */, 4CEFEDAB22B4AF42002CEE19 /* TDCPreferencesUserStyleSheetPrivate.h in Headers */, 4C3155EE20EB6CB400448776 /* TVCMemberListAppearance.h in Headers */, 4C3158E320EB6D1600448776 /* GTMEncodeHTML.h in Headers */, 4C31572B20EB6D0500448776 /* TDCServerHighlightListSheetPrivate.h in Headers */, 4C06E27220EC4E350055D09A /* StaticDefinitions.h in Headers */, 4C31577120EB6D0500448776 /* TXGlobalModelsPrivate.h in Headers */, 4C31577620EB6D0500448776 /* WKWebViewPrivate.h in Headers */, 4C31571620EB6D0500448776 /* TDCFileTransferDialogTransferControllerPrivate.h in Headers */, 4C31575720EB6D0500448776 /* TVCLogViewInternalWK1.h in Headers */, 4C3155CF20EB6CB400448776 /* TLOSoundPlayer.h in Headers */, 4C3155D020EB6CB400448776 /* TPCApplicationInfo.h in Headers */, 4C31571420EB6D0500448776 /* TDCFileTransferDialogPrivate.h in Headers */, 4C3156F020EB6D0500448776 /* IRCCommandIndexPrivate.h in Headers */, 4C3155F420EB6CB400448776 /* TXAppearance.h in Headers */, 4C31574A20EB6D0500448776 /* TPCThemePrivate.h in Headers */, 4C3155A420EB6CB400448776 /* IRCAddressBook.h in Headers */, 4C31573E20EB6D0500448776 /* TLOSpeechSynthesizerPrivate.h in Headers */, 4C06E24A20EC4B970055D09A /* IRCConnectionConfig.h in Headers */, 4C3155CC20EB6CB400448776 /* TLOLinkParser.h in Headers */, 4C06E3F620EC51380055D09A /* RCMConnectionManagerProtocol.h in Headers */, 4C3156EC20EB6D0500448776 /* IRCClientConfigPrivate.h in Headers */, 4C31577220EB6D0500448776 /* TXMasterControllerPrivate.h in Headers */, 4C31576020EB6D0500448776 /* TVCMainWindowSplitViewPrivate.h in Headers */, 4C31576720EB6D0500448776 /* TVCMemberListUserInfoPopoverPrivate.h in Headers */, 4C31570C20EB6D0500448776 /* TDCChannelModifyTopicSheetPrivate.h in Headers */, 4C3156FE20EB6D0500448776 /* IRCWorldPrivate.h in Headers */, 4C3155F120EB6CB400448776 /* TVCTextViewWithIRCFormatter.h in Headers */, 4C31576D20EB6D0500448776 /* TVCTextViewWithIRCFormatterPrivate.h in Headers */, 4C31575920EB6D0500448776 /* TVCLogViewPrivate.h in Headers */, 4C31576C20EB6D0500448776 /* TVCTextFormatterMenuPrivate.h in Headers */, 4C3155BF20EB6CB400448776 /* NSViewHelper.h in Headers */, 4C31576220EB6D0500448776 /* TVCMainWindowTextViewPrivate.h in Headers */, 4C31572620EB6D0500448776 /* TDCProgressIndicatorSheetPrivate.h in Headers */, 4C06E3D620EC51380055D09A /* ICLPayload.h in Headers */, 4C8F3FD82C31AF7700118AAF /* THOPluginManager.h in Headers */, 4C31575120EB6D0500448776 /* TVCLogControllerInlineMediaServicePrivate.h in Headers */, 4C31572320EB6D0500448776 /* TDCNicknameColorSheetPrivate.h in Headers */, 4C3156F320EB6D0500448776 /* IRCHighlightLogEntryPrivate.h in Headers */, 4C3158C620EB6D1000448776 /* IRCChannelConfigInternal.h in Headers */, 4C3155E120EB6CB400448776 /* TVCErrorMessagePopoverControllerPrivate.h in Headers */, 4C31571220EB6D0500448776 /* TDCChannelSpotlightSearchResultPrivate.h in Headers */, 4C31573520EB6D0500448776 /* TLOEncryptionManagerPrivate.h in Headers */, 4C3156E420EB6D0500448776 /* WebScriptObjectHelperPrivate.h in Headers */, 4C31576420EB6D0500448776 /* TVCMemberListAppearancePrivate.h in Headers */, 4C3155D820EB6CB400448776 /* TPCResourceManager.h in Headers */, 4C31573D20EB6D0500448776 /* TLONotificationConfigurationPrivate.h in Headers */, 4C31570A20EB6D0500448776 /* TDCChannelInviteSheetPrivate.h in Headers */, 4C3155E620EB6CB400448776 /* TVCLogView.h in Headers */, 4C3155A320EB6CB400448776 /* IRC.h in Headers */, 4C3155C120EB6CB400448776 /* TDCInputPrompt.h in Headers */, 4C31572220EB6D0500448776 /* TDCLicenseUpgradeEligibilitySheetPrivate.h in Headers */, 4C31575A20EB6D0500448776 /* TVCMainWindowAppearancePrivate.h in Headers */, 4C06E27E20EC4E870055D09A /* TPCPreferencesUserDefaults.h in Headers */, 4C3156F420EB6D0500448776 /* IRCISupportInfoPrivate.h in Headers */, 4C5274E320F8D78800B18F9D /* IRCConnectionErrors.h in Headers */, 4C06E27A20EC4E870055D09A /* TLOTimer.h in Headers */, 4C3155A920EB6CB400448776 /* IRCChannelUser.h in Headers */, 4C31576920EB6D0500448776 /* TVCServerListAppearancePrivate.h in Headers */, 4C31571020EB6D0500448776 /* TDCChannelSpotlightControllerPrivate.h in Headers */, 4C31573C20EB6D0500448776 /* TLONicknameCompletionStatusPrivate.h in Headers */, 4C3156F620EB6D0500448776 /* IRCMessagePrivate.h in Headers */, 4C3156EA20EB6D0500448776 /* IRCChannelPrivate.h in Headers */, 4C31573620EB6D0500448776 /* TLOFileLoggerPrivate.h in Headers */, 4C3155A820EB6CB400448776 /* IRCChannelMode.h in Headers */, 4C31574E20EB6D0500448776 /* TVCContentNavigationOutlineViewPrivate.h in Headers */, 4C3155B920EB6CB400448776 /* IRCTreeItem.h in Headers */, 4C06E26920EC4C560055D09A /* TPCPreferencesUserDefaultsPrivate.h in Headers */, 4C3155E920EB6CB400448776 /* TVCMainWindowLoadingScreen.h in Headers */, 4C3158C820EB6D1000448776 /* TVCLogLineInternal.h in Headers */, 4C3155BB20EB6CB400448776 /* IRCUserRelations.h in Headers */, 4C31575C20EB6D0500448776 /* TVCMainWindowLoadingScreenPrivate.h in Headers */, 4C3155B820EB6CB400448776 /* IRCServer.h in Headers */, 4C31570220EB6D0500448776 /* SwiftBridgingHeaderPrivate.h in Headers */, 4C3156EE20EB6D0500448776 /* IRCClientRequestedCommandsPrivate.h in Headers */, 4C3155B520EB6CB400448776 /* IRCNumerics.h in Headers */, 4C3155B620EB6CB400448776 /* IRCPrefix.h in Headers */, 4C31575020EB6D0500448776 /* TVCLogControllerHistoricLogFilePrivate.h in Headers */, 4CAB276D253863D9009B1F07 /* IRCChannelMemberListControllerPrivate.h in Headers */, 4C06E41820EC52D60055D09A /* ICLPayloadLocalPrivate.h in Headers */, 4C3155D620EB6CB400448776 /* TPCPreferencesReload.h in Headers */, 4C3155C920EB6CB400448776 /* TLONotificationController.h in Headers */, 4C31572820EB6D0500448776 /* TDCServerChannelListDialogPrivate.h in Headers */, 4C3155A520EB6CB400448776 /* IRCAddressBookUserTracking.h in Headers */, 4C31573720EB6D0500448776 /* TLONotificationControllerPrivate.h in Headers */, 4C31572920EB6D0500448776 /* TDCServerEndpointListSheetPrivate.h in Headers */, 4C31576320EB6D0500448776 /* TVCMainWindowTitlebarAccessoryViewPrivate.h in Headers */, 4C31577020EB6D0500448776 /* TXApplicationPrivate.h in Headers */, 4C31575D20EB6D0500448776 /* TVCMainWindowPrivate.h in Headers */, 4C3158C120EB6D1000448776 /* IRCAddressBookInternal.h in Headers */, 4C3155F520EB6CB400448776 /* TXAppearanceHelper.h in Headers */, 4C3155AB20EB6CB400448776 /* IRCClientConfig.h in Headers */, 4C31574B20EB6D0500448776 /* TVCAppearancePrivate.h in Headers */, 4C31573020EB6D0500448776 /* THOPluginDispatcherPrivate.h in Headers */, 4C3155BD20EB6CB400448776 /* NSColorHelper.h in Headers */, 4C3155B020EB6CB400448776 /* IRCHighlightMatchCondition.h in Headers */, 4C3155DD20EB6CB400448776 /* TVCAutoExpandingTextField.h in Headers */, 4C3155F020EB6CB400448776 /* TVCServerListAppearance.h in Headers */, 4C31576520EB6D0500448776 /* TVCMemberListCellPrivate.h in Headers */, 4C3156FD20EB6D0500448776 /* IRCUserRelationsPrivate.h in Headers */, 4C31574D20EB6D0500448776 /* TVCChannelSelectionViewControllerPrivate.h in Headers */, 4C31575520EB6D0500448776 /* TVCLogPolicyPrivate.h in Headers */, 4C3155C720EB6CB400448776 /* THOUnicodeHelper.h in Headers */, 4C3155E520EB6CB400448776 /* TVCLogRenderer.h in Headers */, 4C3155F320EB6CB400448776 /* TVCValidatedTextField.h in Headers */, 4C3155C820EB6CB400448776 /* TLOEncryptionManager.h in Headers */, 4C3155F720EB6CB400448776 /* TXMasterController.h in Headers */, 4C31570920EB6D0500448776 /* TDCChannelBanListSheetPrivate.h in Headers */, 4C3156ED20EB6D0500448776 /* IRCClientPrivate.h in Headers */, 4C31572720EB6D0500448776 /* TDCServerChangeNicknameSheetPrivate.h in Headers */, 4C3155D520EB6CB400448776 /* TPCPreferencesLocal.h in Headers */, 4CAB27592537EE66009B1F07 /* IRCChannelMemberListPrivate.h in Headers */, 4C31572120EB6D0500448776 /* TDCLicenseUpgradeDialogPrivate.h in Headers */, 4C3155DC20EB6CB400448776 /* TVCAppearance.h in Headers */, 4C3155CD20EB6CB400448776 /* TLOpenLink.h in Headers */, 4C06E28620EC4E870055D09A /* TLOLocalization.h in Headers */, 4C3158CC20EB6D1000448776 /* TDCFileTransferDialogInternal.h in Headers */, 4C3155A720EB6CB400448776 /* IRCChannelConfig.h in Headers */, 4C3155EA20EB6CB400448776 /* TVCMainWindowSplitView.h in Headers */, 4C31574C20EB6D0500448776 /* TVCChannelSelectionOutlineViewCellPrivate.h in Headers */, 4C31570E20EB6D0500448776 /* TDCChannelPropertiesSheetPrivate.h in Headers */, 4C31570420EB6D0500448776 /* TDCAddressBookSheetPrivate.h in Headers */, 4C31575220EB6D0500448776 /* TVCLogControllerOperationQueuePrivate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 4C1DC5511580420500A47BC9 /* Textual (Standard Release) */ = { isa = PBXNativeTarget; buildConfigurationList = 4C1DC5701580420500A47BC9 /* Build configuration list for PBXNativeTarget "Textual (Standard Release)" */; buildPhases = ( 4C1DC5501580420500A47BC9 /* Resources */, 4CEA6AA42C36F8E700A36551 /* Copy Preference Resources */, 4C9E534415B4C171000E84A9 /* Copy Acknowledgements */, 4C1DC54F1580420500A47BC9 /* Frameworks */, 4C1DC54E1580420500A47BC9 /* Sources */, 4CCF2F9E15804C18006FFE21 /* Headers */, 4C06E48020EC541C0055D09A /* Embed Frameworks */, 4C2BEB612C384E16008F8F0D /* Postprocess Sparkle */, 4C01BA101581104D007E2DAF /* Build Extensions */, 4C52AED515811C2000940619 /* Copy Extensions */, 4C761BAF1E155D7400A505B7 /* Build XPC Services */, 4C761BA61E155C6E00A505B7 /* Copy XPC Services */, 4C48B46620E4D38F00CF5F84 /* Merge Swift Libraries */, ); buildRules = ( ); dependencies = ( 4CBB842D16F0DA1D004E3ED6 /* PBXTargetDependency */, ); name = "Textual (Standard Release)"; productName = Textual; productReference = 4C1DC5521580420500A47BC9 /* Textual.app */; productType = "com.apple.product-type.application"; }; 4C5BA37216F1302F00A96CA2 /* Textual (Debug) */ = { isa = PBXNativeTarget; buildConfigurationList = 4C5BA4E116F1302F00A96CA2 /* Build configuration list for PBXNativeTarget "Textual (Debug)" */; buildPhases = ( 4C5BA37916F1302F00A96CA2 /* Resources */, 4CEA6AA92C36F91400A36551 /* Copy Preference Resources */, 4C5BA4DC16F1302F00A96CA2 /* Copy Acknowledgements */, 4C5BA45716F1302F00A96CA2 /* Frameworks */, 4C0444F316F1602900EBB665 /* Sources */, 4C5BA3D916F1302F00A96CA2 /* Headers */, 4C06E44320EC539E0055D09A /* Embed Frameworks */, 4CEE0BDB268F3C68004CB3C3 /* Build Extensions */, 4CEE0BDC268F3C8D004CB3C3 /* Copy Extensions */, 4C761BB01E155DD900A505B7 /* Build XPC Services */, 4C761BA41E155C4300A505B7 /* Copy XPC Services */, 4C48B46520E4D37C00CF5F84 /* Merge Swift Libraries */, ); buildRules = ( ); dependencies = ( ); name = "Textual (Debug)"; productName = Textual; productReference = 4C5BA4E616F1302F00A96CA2 /* Textual.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 4C1DC5491580420500A47BC9 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Codeux Software, LLC"; TargetAttributes = { 4C1DC5511580420500A47BC9 = { DevelopmentTeam = 8482Q6EPL6; DevelopmentTeamName = "Codeux Software, LLC"; LastSwiftMigration = 1100; ProvisioningStyle = Manual; SystemCapabilities = { com.apple.ApplicationGroups.Mac = { enabled = 1; }; }; }; 4C5BA37216F1302F00A96CA2 = { DevelopmentTeam = 8482Q6EPL6; DevelopmentTeamName = "Codeux Software, LLC"; LastSwiftMigration = 1100; ProvisioningStyle = Manual; SystemCapabilities = { com.apple.ApplicationGroups.Mac = { enabled = 1; }; }; }; 4CCF301015804DCE006FFE21 = { DevelopmentTeam = 8482Q6EPL6; }; }; }; buildConfigurationList = 4C1DC54C1580420500A47BC9 /* Build configuration list for PBXProject "Textual App" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 4C1DC5471580420500A47BC9; productRefGroup = 4C1DC5531580420500A47BC9 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4C5BA37216F1302F00A96CA2 /* Textual (Debug) */, 4C1DC5511580420500A47BC9 /* Textual (Standard Release) */, 4CCF301015804DCE006FFE21 /* Build Frameworks */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4C1DC5501580420500A47BC9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C06E07820EC4A540055D09A /* FormattingColor_8.png in Resources */, 4C06E1DC20EC4AC40055D09A /* TDCNicknameColorSheet.xib in Resources */, 4C06E20820EC4AC50055D09A /* TDCLicenseManagerDialog.xib in Resources */, 4C06E11C20EC4A630055D09A /* TPWTB_Style@2x.png in Resources */, 4C06DFE520EC49FB0055D09A /* MainWindowSpotlightIconTemplate.pdf in Resources */, 4CEFEDB222B4B6E8002CEE19 /* TDCPreferencesUserStyleSheet.strings in Resources */, 4C06E2EE20EC4FF00055D09A /* BasicLanguage.strings in Resources */, 4C06E08320EC4A540055D09A /* FormattingColor_4.png in Resources */, 4CAC9D7B1F2B076100AD5F12 /* Textual-Extras.pkg in Resources */, 4C06E0D120EC4A5B0055D09A /* channelRoomStatusIconLightInactive@2x.tif in Resources */, 4C06E21020EC4AC50055D09A /* TVCMemberListAppearance.plist in Resources */, 4C06E07B20EC4A540055D09A /* FormattingColor_10.png in Resources */, 4C06E08020EC4A540055D09A /* FormattingColor_15.png in Resources */, 4C06E11D20EC4A630055D09A /* TPWTB_General@2x.png in Resources */, 4C06E32A20EC4FF00055D09A /* TVCNotificationConfigurationView.strings in Resources */, 4C06E08520EC4A540055D09A /* FormattingColor_1.png in Resources */, 4C06E02020EC4A060055D09A /* DIRedBadgeRight.png in Resources */, 4C06E1C420EC4AC40055D09A /* TVCChannelSelectionView.xib in Resources */, 4C06E03920EC4A0F0055D09A /* DIGreenBadgeRight.png in Resources */, 4C06E04E20EC4A2F0055D09A /* applicationIcon.iconset in Resources */, 4C06E1FC20EC4AC50055D09A /* TDCAboutDialog.xib in Resources */, 4C06E05320EC4A3D0055D09A /* Copyright Information for Images.txt in Resources */, 4C06E07A20EC4A540055D09A /* FormattingColor_11.png in Resources */, 4C06E1CC20EC4AC40055D09A /* TVCNotificationConfigurationView.xib in Resources */, 4C06DF3520EC49A60055D09A /* IRCNetworks.plist in Resources */, 4C06E1EC20EC4AC50055D09A /* TVCAlert.xib in Resources */, 4C06E32220EC4FF00055D09A /* Notifications.strings in Resources */, 4C06E2F620EC4FF00055D09A /* TDCChannelPropertiesSheet.strings in Resources */, 4C06E32E20EC4FF00055D09A /* TDCServerEndpointListSheet.strings in Resources */, 4C06E31220EC4FF00055D09A /* IRC.strings in Resources */, 4C06E1E420EC4AC40055D09A /* TDCProgressIndicatorSheet.xib in Resources */, 4C06E34220EC4FF00055D09A /* TDCChannelModifyTopicSheet.strings in Resources */, 4C06DFE620EC49FB0055D09A /* ErroneousTextFieldValueIndicator.tif in Resources */, 4C06E03820EC4A0F0055D09A /* DIGreenBadgeLeft.png in Resources */, 4C06DF4420EC49B90055D09A /* RemoteLicenseSystemPublicKey.pub in Resources */, 4C06E19C20EC4AC40055D09A /* TDCChannelModifyModesSheet.xib in Resources */, 4C06E0CD20EC4A5B0055D09A /* channelRoomStatusIconLightActive.tif in Resources */, 4C06E0CC20EC4A5B0055D09A /* channelRoomStatusIconDarkActive.tif in Resources */, 4C06E11820EC4A630055D09A /* TPWTB_Highlights@2x.png in Resources */, 4C06E08120EC4A540055D09A /* FormattingColor_7.png in Resources */, 4C17305622B4A94000DC5836 /* TDCPreferencesUserStyleSheet.xib in Resources */, 4C06E1D820EC4AC40055D09A /* TDCChannelInviteSheet.xib in Resources */, 4C06E03A20EC4A0F0055D09A /* DIGreenBadgeCenter.png in Resources */, 4C06E19820EC4AC40055D09A /* TDCServerEndpointListSheet.xib in Resources */, 4C06DF3920EC49A60055D09A /* TemplateLineTypes.plist in Resources */, 4C06E11920EC4A630055D09A /* TPWTB_Addons.png in Resources */, 4C06E2F220EC4FF00055D09A /* Accessibility.strings in Resources */, 4C06E11B20EC4A630055D09A /* TPWTB_Controls.png in Resources */, 4C33341E20CC471A00ACB9AD /* Bundled Scripts in Resources */, 4C06DF3420EC49A60055D09A /* IRCCommandIndexRemoteData.plist in Resources */, 4C33342320CC475300ACB9AD /* Bundled Styles in Resources */, 4C06E07C20EC4A540055D09A /* FormattingColor_12.png in Resources */, 4C06E1D020EC4AC40055D09A /* TDCLicenseUpgradeActivateSheet.xib in Resources */, 4C06E12020EC4A630055D09A /* TPWTB_Controls@2x.png in Resources */, 4C06E20C20EC4AC50055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.xib in Resources */, 4C06E1AC20EC4AC40055D09A /* TDCChannelBanListSheet.xib in Resources */, 4C06E12220EC4A630055D09A /* TPWTB_Interface@2x.png in Resources */, 4C06E2FE20EC4FF00055D09A /* TDCPreferencesController.strings in Resources */, 4C06E1A820EC4AC40055D09A /* TDCServerChannelListDialog.xib in Resources */, 4C06E0C820EC4A5B0055D09A /* channelRoomStatusIconDarkActive@2x.tif in Resources */, 4C06E30620EC4FF00055D09A /* TDCChannelInviteSheet.strings in Resources */, 4C06E22820EC4AC50055D09A /* TVCMainWindowAppearance.plist in Resources */, 4C06E05D20EC4A4C0055D09A /* encryptionLockIconLight.tif in Resources */, 4C06E11A20EC4A630055D09A /* TPWTB_Advanced.png in Resources */, 4C06E08620EC4A540055D09A /* FormattingColor_Rainbow@2x.tif in Resources */, 4C06E19420EC4AC40055D09A /* TDCServerHighlightListSheet.xib in Resources */, 4C06E0CA20EC4A5B0055D09A /* channelRoomStatusIconDarkInactive@2x.tif in Resources */, 4C06E0C720EC4A5B0055D09A /* channelRoomStatusIconLightActive@2x.tif in Resources */, 4C06E1C820EC4AC40055D09A /* TXCMainMenu.xib in Resources */, 4C06E23020EC4AC50055D09A /* TVCServerListAppearance.plist in Resources */, 4C06E30A20EC4FF00055D09A /* TDCAboutDialog.strings in Resources */, 4C06E0C920EC4A5B0055D09A /* channelRoomStatusIconLightInactive.tif in Resources */, 4C06E2E220EC4FF00055D09A /* OffTheRecord.strings in Resources */, 4C06DFEB20EC49FB0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconActive.tif in Resources */, 4C06E2E620EC4FF00055D09A /* CommonErrors.strings in Resources */, 4C06E30E20EC4FF00055D09A /* TDCServerPropertiesSheet.strings in Resources */, 4C06E2FA20EC4FF00055D09A /* TDCChannelBanListSheet.strings in Resources */, 4C06E1B420EC4AC40055D09A /* TDCLicenseManagerMigrateAppStoreSheet.xib in Resources */, 4C06E07F20EC4A540055D09A /* FormattingColor_14.png in Resources */, 4C06E08720EC4A540055D09A /* FormattingColor_0.png in Resources */, 4C06DFED20EC49FB0055D09A /* VibrantLightServerListViewPrivateMessageUserIconInactive.tif in Resources */, 4C06E1D420EC4AC40055D09A /* TDCPreferences.xib in Resources */, 4C06E04F20EC4A2F0055D09A /* applicationIconBirthday.icns in Resources */, 4C06E02120EC4A060055D09A /* DIRedBadgeLeft.png in Resources */, 4C06E02220EC4A060055D09A /* DIRedBadgeCenter.png in Resources */, 4C06E18C20EC4AC40055D09A /* TDCServerPropertiesSheet.xib in Resources */, 4C06E20020EC4AC50055D09A /* TDCHighlightEntrySheet.xib in Resources */, 4C06DF3820EC49A60055D09A /* StaticStore.plist in Resources */, 4C06E12120EC4A630055D09A /* TPWTB_Interface.png in Resources */, 4C06E22C20EC4AC50055D09A /* TVCMainWindowTextViewAppearance.plist in Resources */, 4C06E11E20EC4A630055D09A /* TPWTB_Addons@2x.png in Resources */, 4C06E08820EC4A540055D09A /* FormattingColor_2.png in Resources */, 4CCD93FF1CA7291300335381 /* JavaScript in Resources */, 4C06E07D20EC4A540055D09A /* FormattingColor_Rainbow.tif in Resources */, 4C06E31A20EC4FF00055D09A /* TDCChannelModifyModesSheet.strings in Resources */, 4C06E2DE20EC4FF00055D09A /* Prompts.strings in Resources */, 4C06E33A20EC4FF00055D09A /* TDCAddressBookSheet.strings in Resources */, 4C06E1B020EC4AC40055D09A /* TDCChannelPropertiesSheet.xib in Resources */, 4C06E05C20EC4A4C0055D09A /* encryptionLockIconDark@2x.tif in Resources */, 4C06E1F820EC4AC50055D09A /* TDCChannelModifyTopicSheet.xib in Resources */, 4C06E1F420EC4AC50055D09A /* TDCServerChangeNicknameSheet.xib in Resources */, 4C06E07E20EC4A540055D09A /* FormattingColor_13.png in Resources */, 4C06E08220EC4A540055D09A /* FormattingColor_6.png in Resources */, 4C06E12420EC4A630055D09A /* TPWTB_Style.png in Resources */, 4C06E1E820EC4AC50055D09A /* TDCAddressBookSheet.xib in Resources */, 4C06E1A020EC4AC40055D09A /* TDCChannelSpotlightController.xib in Resources */, 4C06E0CF20EC4A5B0055D09A /* channelRoomStatusIconDarkInactive.tif in Resources */, 4C06E05A20EC4A4C0055D09A /* encryptionLockIconLight@2x.tif in Resources */, 4C06E05B20EC4A4C0055D09A /* encryptionLockIconDark.tif in Resources */, 4C06E31E20EC4FF00055D09A /* TVCMainWindow.strings in Resources */, 4C06E08920EC4A540055D09A /* FormattingColor_3.png in Resources */, 4C06DFE820EC49FB0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif in Resources */, 4C06E11F20EC4A630055D09A /* TPWTB_Notifications@2x.png in Resources */, 4C06E33E20EC4FF00055D09A /* TDCLicenseUpgradeEligibilitySheet.strings in Resources */, 4C06E11720EC4A630055D09A /* TPWTB_Highlights.png in Resources */, 4C06DFE420EC49FB0055D09A /* MainWindowSegmentedControlUserTemplate.pdf in Resources */, 4C06E07920EC4A540055D09A /* FormattingColor_9.png in Resources */, 4C52379415C18F6700414852 /* Style Default Templates in Resources */, 4C06E30220EC4FF00055D09A /* TDCFileTransferDialog.strings in Resources */, 4C06DFEE20EC49FB0055D09A /* VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif in Resources */, 4C06E31620EC4FF00055D09A /* TLOLicenseManager.strings in Resources */, 4C06E12320EC4A630055D09A /* TPWTB_Notifications.png in Resources */, 4C06E1E020EC4AC40055D09A /* TDCWelcomeSheet.xib in Resources */, 4C06E12520EC4A630055D09A /* TPWTB_General.png in Resources */, 4C06E33220EC4FF00055D09A /* TDCServerChannelListDialog.strings in Resources */, 4C06E33620EC4FF00055D09A /* TDCChannelSpotlightController.strings in Resources */, 4C06E1A420EC4AC40055D09A /* TDCFileTransferDialog.xib in Resources */, 4C06DFEC20EC49FB0055D09A /* VibrantLightServerListViewPrivateMessageUserIconActive.tif in Resources */, 4C06DFE720EC49FB0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif in Resources */, 4C06E08420EC4A540055D09A /* FormattingColor_5.png in Resources */, 4C06E11620EC4A630055D09A /* TPWTB_Advanced@2x.png in Resources */, 4C06E1F020EC4AC50055D09A /* TDCLicenseUpgradeDialog.xib in Resources */, 4C06DF3320EC49A60055D09A /* IRCCommandIndexLocalData.plist in Resources */, 4C06E22420EC4AC50055D09A /* TDCChannelSpotlightAppearance.plist in Resources */, 4C06E1BC20EC4AC40055D09A /* TDCLicenseUpgradeEligibilitySheet.xib in Resources */, 4C06E20420EC4AC50055D09A /* TVCMainWindow.xib in Resources */, 4C06DFEF20EC49FB0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconInactive.tif in Resources */, 4C06DFF020EC49FB0055D09A /* VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 4C5BA37916F1302F00A96CA2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C06E08A20EC4A540055D09A /* FormattingColor_8.png in Resources */, 4C06E1DB20EC4AC40055D09A /* TDCNicknameColorSheet.xib in Resources */, 4C06E20720EC4AC50055D09A /* TDCLicenseManagerDialog.xib in Resources */, 4C06E10C20EC4A630055D09A /* TPWTB_Style@2x.png in Resources */, 4C06DFCA20EC49FA0055D09A /* MainWindowSpotlightIconTemplate.pdf in Resources */, 4CEFEDB122B4B6E8002CEE19 /* TDCPreferencesUserStyleSheet.strings in Resources */, 4C06E2ED20EC4FF00055D09A /* BasicLanguage.strings in Resources */, 4C06E09520EC4A540055D09A /* FormattingColor_4.png in Resources */, 4CAC9D831F2B076400AD5F12 /* Textual-Extras.pkg in Resources */, 4C06E0BF20EC4A5A0055D09A /* channelRoomStatusIconLightInactive@2x.tif in Resources */, 4C06E20F20EC4AC50055D09A /* TVCMemberListAppearance.plist in Resources */, 4C06E08D20EC4A540055D09A /* FormattingColor_10.png in Resources */, 4C06E09220EC4A540055D09A /* FormattingColor_15.png in Resources */, 4C06E10D20EC4A630055D09A /* TPWTB_General@2x.png in Resources */, 4C06E32920EC4FF00055D09A /* TVCNotificationConfigurationView.strings in Resources */, 4C06E09720EC4A540055D09A /* FormattingColor_1.png in Resources */, 4C06E02620EC4A060055D09A /* DIRedBadgeRight.png in Resources */, 4C06E1C320EC4AC40055D09A /* TVCChannelSelectionView.xib in Resources */, 4C06E03F20EC4A100055D09A /* DIGreenBadgeRight.png in Resources */, 4C06E04C20EC4A2E0055D09A /* applicationIcon.iconset in Resources */, 4C06E1FB20EC4AC50055D09A /* TDCAboutDialog.xib in Resources */, 4C06E05420EC4A3E0055D09A /* Copyright Information for Images.txt in Resources */, 4C06E08C20EC4A540055D09A /* FormattingColor_11.png in Resources */, 4C06E1CB20EC4AC40055D09A /* TVCNotificationConfigurationView.xib in Resources */, 4C06DF2C20EC49A60055D09A /* IRCNetworks.plist in Resources */, 4C06E1EB20EC4AC50055D09A /* TVCAlert.xib in Resources */, 4C06E32120EC4FF00055D09A /* Notifications.strings in Resources */, 4C06E2F520EC4FF00055D09A /* TDCChannelPropertiesSheet.strings in Resources */, 4C06E32D20EC4FF00055D09A /* TDCServerEndpointListSheet.strings in Resources */, 4C06E31120EC4FF00055D09A /* IRC.strings in Resources */, 4C06E1E320EC4AC40055D09A /* TDCProgressIndicatorSheet.xib in Resources */, 4C06E34120EC4FF00055D09A /* TDCChannelModifyTopicSheet.strings in Resources */, 4C06DFCB20EC49FA0055D09A /* ErroneousTextFieldValueIndicator.tif in Resources */, 4C06E03E20EC4A100055D09A /* DIGreenBadgeLeft.png in Resources */, 4C06DF4320EC49B80055D09A /* RemoteLicenseSystemPublicKey.pub in Resources */, 4C06E19B20EC4AC40055D09A /* TDCChannelModifyModesSheet.xib in Resources */, 4C06E0BB20EC4A5A0055D09A /* channelRoomStatusIconLightActive.tif in Resources */, 4C06E0BA20EC4A5A0055D09A /* channelRoomStatusIconDarkActive.tif in Resources */, 4C06E10820EC4A630055D09A /* TPWTB_Highlights@2x.png in Resources */, 4C06E09320EC4A540055D09A /* FormattingColor_7.png in Resources */, 4C17305522B4A94000DC5836 /* TDCPreferencesUserStyleSheet.xib in Resources */, 4C06E1D720EC4AC40055D09A /* TDCChannelInviteSheet.xib in Resources */, 4C06E04020EC4A100055D09A /* DIGreenBadgeCenter.png in Resources */, 4C06E19720EC4AC40055D09A /* TDCServerEndpointListSheet.xib in Resources */, 4C06DF3020EC49A60055D09A /* TemplateLineTypes.plist in Resources */, 4C06E10920EC4A630055D09A /* TPWTB_Addons.png in Resources */, 4C06E2F120EC4FF00055D09A /* Accessibility.strings in Resources */, 4C06E10B20EC4A630055D09A /* TPWTB_Controls.png in Resources */, 4CCD93FE1CA7291300335381 /* JavaScript in Resources */, 4C06DF2B20EC49A60055D09A /* IRCCommandIndexRemoteData.plist in Resources */, 4C33341D20CC471A00ACB9AD /* Bundled Scripts in Resources */, 4C06E08E20EC4A540055D09A /* FormattingColor_12.png in Resources */, 4C06E1CF20EC4AC40055D09A /* TDCLicenseUpgradeActivateSheet.xib in Resources */, 4C06E11020EC4A630055D09A /* TPWTB_Controls@2x.png in Resources */, 4C06E20B20EC4AC50055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.xib in Resources */, 4C06E1AB20EC4AC40055D09A /* TDCChannelBanListSheet.xib in Resources */, 4C06E11220EC4A630055D09A /* TPWTB_Interface@2x.png in Resources */, 4C06E2FD20EC4FF00055D09A /* TDCPreferencesController.strings in Resources */, 4C06E1A720EC4AC40055D09A /* TDCServerChannelListDialog.xib in Resources */, 4C06E0B620EC4A5A0055D09A /* channelRoomStatusIconDarkActive@2x.tif in Resources */, 4C06E30520EC4FF00055D09A /* TDCChannelInviteSheet.strings in Resources */, 4C06E22720EC4AC50055D09A /* TVCMainWindowAppearance.plist in Resources */, 4C06E06120EC4A4C0055D09A /* encryptionLockIconLight.tif in Resources */, 4C06E10A20EC4A630055D09A /* TPWTB_Advanced.png in Resources */, 4C06E09820EC4A540055D09A /* FormattingColor_Rainbow@2x.tif in Resources */, 4C06E19320EC4AC40055D09A /* TDCServerHighlightListSheet.xib in Resources */, 4C06E0B820EC4A5A0055D09A /* channelRoomStatusIconDarkInactive@2x.tif in Resources */, 4C06E0B520EC4A5A0055D09A /* channelRoomStatusIconLightActive@2x.tif in Resources */, 4C06E1C720EC4AC40055D09A /* TXCMainMenu.xib in Resources */, 4C06E22F20EC4AC50055D09A /* TVCServerListAppearance.plist in Resources */, 4C06E30920EC4FF00055D09A /* TDCAboutDialog.strings in Resources */, 4C06E0B720EC4A5A0055D09A /* channelRoomStatusIconLightInactive.tif in Resources */, 4C06E2E120EC4FF00055D09A /* OffTheRecord.strings in Resources */, 4C06DFD020EC49FA0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconActive.tif in Resources */, 4C06E2E520EC4FF00055D09A /* CommonErrors.strings in Resources */, 4C06E30D20EC4FF00055D09A /* TDCServerPropertiesSheet.strings in Resources */, 4C06E2F920EC4FF00055D09A /* TDCChannelBanListSheet.strings in Resources */, 4C06E1B320EC4AC40055D09A /* TDCLicenseManagerMigrateAppStoreSheet.xib in Resources */, 4C06E09120EC4A540055D09A /* FormattingColor_14.png in Resources */, 4C06E09920EC4A540055D09A /* FormattingColor_0.png in Resources */, 4C06DFD220EC49FA0055D09A /* VibrantLightServerListViewPrivateMessageUserIconInactive.tif in Resources */, 4C06E1D320EC4AC40055D09A /* TDCPreferences.xib in Resources */, 4C06E04D20EC4A2E0055D09A /* applicationIconBirthday.icns in Resources */, 4C06E02720EC4A060055D09A /* DIRedBadgeLeft.png in Resources */, 4C06E02820EC4A060055D09A /* DIRedBadgeCenter.png in Resources */, 4C06E18B20EC4AC40055D09A /* TDCServerPropertiesSheet.xib in Resources */, 4C06E1FF20EC4AC50055D09A /* TDCHighlightEntrySheet.xib in Resources */, 4C06DF2F20EC49A60055D09A /* StaticStore.plist in Resources */, 4C06E11120EC4A630055D09A /* TPWTB_Interface.png in Resources */, 4C06E22B20EC4AC50055D09A /* TVCMainWindowTextViewAppearance.plist in Resources */, 4C06E10E20EC4A630055D09A /* TPWTB_Addons@2x.png in Resources */, 4C06E09A20EC4A540055D09A /* FormattingColor_2.png in Resources */, 4C33342220CC475300ACB9AD /* Bundled Styles in Resources */, 4C06E08F20EC4A540055D09A /* FormattingColor_Rainbow.tif in Resources */, 4C06E31920EC4FF00055D09A /* TDCChannelModifyModesSheet.strings in Resources */, 4C06E2DD20EC4FF00055D09A /* Prompts.strings in Resources */, 4C06E33920EC4FF00055D09A /* TDCAddressBookSheet.strings in Resources */, 4C06E1AF20EC4AC40055D09A /* TDCChannelPropertiesSheet.xib in Resources */, 4C06E06020EC4A4C0055D09A /* encryptionLockIconDark@2x.tif in Resources */, 4C06E1F720EC4AC50055D09A /* TDCChannelModifyTopicSheet.xib in Resources */, 4C06E1F320EC4AC50055D09A /* TDCServerChangeNicknameSheet.xib in Resources */, 4C06E09020EC4A540055D09A /* FormattingColor_13.png in Resources */, 4C06E09420EC4A540055D09A /* FormattingColor_6.png in Resources */, 4C06E11420EC4A630055D09A /* TPWTB_Style.png in Resources */, 4C06E1E720EC4AC50055D09A /* TDCAddressBookSheet.xib in Resources */, 4C06E19F20EC4AC40055D09A /* TDCChannelSpotlightController.xib in Resources */, 4C06E0BD20EC4A5A0055D09A /* channelRoomStatusIconDarkInactive.tif in Resources */, 4C06E05E20EC4A4C0055D09A /* encryptionLockIconLight@2x.tif in Resources */, 4C06E05F20EC4A4C0055D09A /* encryptionLockIconDark.tif in Resources */, 4C06E31D20EC4FF00055D09A /* TVCMainWindow.strings in Resources */, 4C06E09B20EC4A540055D09A /* FormattingColor_3.png in Resources */, 4C06DFCD20EC49FA0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconActive@2x.tif in Resources */, 4C06E10F20EC4A630055D09A /* TPWTB_Notifications@2x.png in Resources */, 4C06E33D20EC4FF00055D09A /* TDCLicenseUpgradeEligibilitySheet.strings in Resources */, 4C06E10720EC4A630055D09A /* TPWTB_Highlights.png in Resources */, 4C06DFC920EC49FA0055D09A /* MainWindowSegmentedControlUserTemplate.pdf in Resources */, 4C06E08B20EC4A540055D09A /* FormattingColor_9.png in Resources */, 4C5BA3CB16F1302F00A96CA2 /* Style Default Templates in Resources */, 4C06E30120EC4FF00055D09A /* TDCFileTransferDialog.strings in Resources */, 4C06DFD320EC49FA0055D09A /* VibrantLightServerListViewPrivateMessageUserIconActive@2x.tif in Resources */, 4C06E31520EC4FF00055D09A /* TLOLicenseManager.strings in Resources */, 4C06E11320EC4A630055D09A /* TPWTB_Notifications.png in Resources */, 4C06E1DF20EC4AC40055D09A /* TDCWelcomeSheet.xib in Resources */, 4C06E11520EC4A630055D09A /* TPWTB_General.png in Resources */, 4C06E33120EC4FF00055D09A /* TDCServerChannelListDialog.strings in Resources */, 4C06E33520EC4FF00055D09A /* TDCChannelSpotlightController.strings in Resources */, 4C06E1A320EC4AC40055D09A /* TDCFileTransferDialog.xib in Resources */, 4C06DFD120EC49FA0055D09A /* VibrantLightServerListViewPrivateMessageUserIconActive.tif in Resources */, 4C06DFCC20EC49FA0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconInactive@2x.tif in Resources */, 4C06E09620EC4A540055D09A /* FormattingColor_5.png in Resources */, 4C06E10620EC4A630055D09A /* TPWTB_Advanced@2x.png in Resources */, 4C06E1EF20EC4AC50055D09A /* TDCLicenseUpgradeDialog.xib in Resources */, 4C06DF2A20EC49A60055D09A /* IRCCommandIndexLocalData.plist in Resources */, 4C06E22320EC4AC50055D09A /* TDCChannelSpotlightAppearance.plist in Resources */, 4C06E1BB20EC4AC40055D09A /* TDCLicenseUpgradeEligibilitySheet.xib in Resources */, 4C06E20320EC4AC50055D09A /* TVCMainWindow.xib in Resources */, 4C06DFD420EC49FA0055D09A /* VibrantDarkServerListViewPrivateMessageUserIconInactive.tif in Resources */, 4C06DFD520EC49FA0055D09A /* VibrantLightServerListViewPrivateMessageUserIconInactive@2x.tif in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 4C01BA101581104D007E2DAF /* Build Extensions */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 12; files = ( ); inputPaths = ( ); name = "Build Extensions"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/BuildExtensions.sh\" | tee \"${TEXTUAL_WORKSPACE_TEMP_DIR}/Script-Logs/BuildExtensions.txt\"\n"; showEnvVarsInLog = 0; }; 4C2BEB612C384E16008F8F0D /* Postprocess Sparkle */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Postprocess Sparkle"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/PostprocessSparkle.sh\" | tee \"${TEXTUAL_WORKSPACE_TEMP_DIR}/Script-Logs/PostprocessSparkle.txt\"\n"; showEnvVarsInLog = 0; }; 4C48B46520E4D37C00CF5F84 /* Merge Swift Libraries */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Merge Swift Libraries"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/MergeSwift.sh\"\n"; }; 4C48B46620E4D38F00CF5F84 /* Merge Swift Libraries */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Merge Swift Libraries"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/MergeSwift.sh\"\n"; }; 4C761BAF1E155D7400A505B7 /* Build XPC Services */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Build XPC Services"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/BuildServices.sh\" | tee \"${TEXTUAL_WORKSPACE_TEMP_DIR}/Script-Logs/BuildServices.txt\"\n"; showEnvVarsInLog = 0; }; 4C761BB01E155DD900A505B7 /* Build XPC Services */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Build XPC Services"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/BuildServices.sh\" | tee \"${TEXTUAL_WORKSPACE_TEMP_DIR}/Script-Logs/BuildServices.txt\"\n"; }; 4CCF301715804DD1006FFE21 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/BuildFrameworks.sh\"\n"; }; 4CEE0BDB268F3C68004CB3C3 /* Build Extensions */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Build Extensions"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/BuildExtensions.sh\" | tee \"${TEXTUAL_WORKSPACE_TEMP_DIR}/Script-Logs/BuildExtensions.txt\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 4C0444F316F1602900EBB665 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C06E57F20EC553A0055D09A /* TXAppearance.m in Sources */, 4C06E58020EC553A0055D09A /* TXApplication.m in Sources */, 4C06E58120EC553A0055D09A /* TXGlobalModels.m in Sources */, 4C06E58220EC553A0055D09A /* TXMasterController.m in Sources */, 4C06E58320EC553A0055D09A /* TXMenuController.m in Sources */, 4C06E58420EC553A0055D09A /* TXSharedApplication.m in Sources */, 4C06E58520EC553A0055D09A /* TXWindowController.m in Sources */, 4C06E58A20EC553A0055D09A /* TDCChannelSpotlightControls.m in Sources */, 4C06E58B20EC553A0055D09A /* TDCChannelSpotlightSearchResult.m in Sources */, 4C06E58C20EC553A0055D09A /* TDCChannelSpotlightController.m in Sources */, 4C06E58D20EC553A0055D09A /* TDCChannelSpotlightAppearance.m in Sources */, 4C06E58E20EC553A0055D09A /* TDCChannelSpotlightSearchResultsTable.m in Sources */, 4C06E58F20EC553A0055D09A /* TDCFileTransferDialog.m in Sources */, 4C06E59020EC553A0055D09A /* TDCFileTransferDialogTransferController.m in Sources */, 4C06E59120EC553A0055D09A /* TDCFileTransferDialogTableCell.m in Sources */, 4C06E59220EC553A0055D09A /* TDCLicenseUpgradeDialog.m in Sources */, 4CAB27522537EE44009B1F07 /* IRCChannelMemberList.m in Sources */, 4C06E59320EC553A0055D09A /* TDCLicenseUpgradeEligibilitySheet.m in Sources */, 4C06E59420EC553A0055D09A /* TDCLicenseManagerMigrateAppStoreSheet.m in Sources */, 4C06E59520EC553A0055D09A /* TDCLicenseUpgradeCommonActions.m in Sources */, 4C06E59620EC553A0055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.m in Sources */, 4C06E59720EC553A0055D09A /* TDCLicenseUpgradeActivateSheet.m in Sources */, 4CAB2766253863BF009B1F07 /* IRCChannelMemberListController.m in Sources */, 4C06E59820EC553A0055D09A /* TDCLicenseManagerDialog.m in Sources */, 4C06E59D20EC553A0055D09A /* TDCPreferencesNotificationConfiguration.m in Sources */, 4C06E59E20EC553A0055D09A /* TDCPreferencesController.m in Sources */, 4C06E59F20EC553A0055D09A /* TDCServerEndpointListSheetTable.m in Sources */, 4C06E5A020EC553A0055D09A /* TDCServerEndpointListSheet.m in Sources */, 4C06E5A120EC553A0055D09A /* TDCAboutDialog.m in Sources */, 4C06E5A220EC553A0055D09A /* TDCAddressBookSheet.m in Sources */, 4C06E5A320EC553A0055D09A /* TDCAlert.m in Sources */, 4C06E5A420EC553A0055D09A /* TDCChannelBanListSheet.m in Sources */, 4C06E5A520EC553A0055D09A /* TDCChannelInviteSheet.m in Sources */, 4C06E5A620EC553A0055D09A /* TDCChannelModifyModesSheet.m in Sources */, 4C06E5A720EC553A0055D09A /* TDCChannelModifyTopicSheet.m in Sources */, 4C06E5A820EC553A0055D09A /* TDCChannelPropertiesNotificationConfiguration.m in Sources */, 4C06E5A920EC553A0055D09A /* TDCChannelPropertiesSheet.m in Sources */, 4C06E5AA20EC553A0055D09A /* TDCHighlightEntrySheet.m in Sources */, 4C06E5AB20EC553A0055D09A /* TDCInputPrompt.m in Sources */, 4C06E5AC20EC553A0055D09A /* TDCNicknameColorSheet.m in Sources */, 4C06E5AD20EC553A0055D09A /* TDCProgressIndicatorSheet.m in Sources */, 4C06E5AE20EC553A0055D09A /* TDCServerChangeNicknameSheet.m in Sources */, 4C06E5AF20EC553A0055D09A /* TDCServerChannelListDialog.m in Sources */, 4C06E5B020EC553A0055D09A /* TDCServerHighlightListSheet.m in Sources */, 4C06E5B120EC553A0055D09A /* TDCServerPropertiesSheet.m in Sources */, 4C06E5B220EC553A0055D09A /* TDCSheetBase.m in Sources */, 4C06E5B320EC553A0055D09A /* TDCWelcomeSheet.m in Sources */, 4C06E5B420EC553A0055D09A /* TDCWindowBase.m in Sources */, 4C06E5B520EC553A0055D09A /* NSColorHelper.m in Sources */, 4C06E5B620EC553A0055D09A /* NSObjectHelper.m in Sources */, 4C06E5B720EC553A0055D09A /* NSStringHelper.m in Sources */, 4C06E5B820EC553A0055D09A /* NSTableViewHelper.m in Sources */, 4C06E5B920EC553A0055D09A /* NSViewHelper.m in Sources */, 4C06E5BA20EC553A0055D09A /* TXAppearanceHelper.m in Sources */, 4C06E5BB20EC553A0055D09A /* WebScriptObjectHelper.m in Sources */, 4C06E5BC20EC553A0055D09A /* GTMEncodeHTML.m in Sources */, 4C06E5BD20EC553A0055D09A /* THOPluginDispatcher.m in Sources */, 4C06E5BE20EC553A0055D09A /* THOPluginManager.m in Sources */, 4C06E5BF20EC553A0055D09A /* THOPluginItem.m in Sources */, 4C06E5C020EC553A0055D09A /* THOUnicodeHelper.m in Sources */, 4C06E5C120EC553A0055D09A /* IRCAddressBook.m in Sources */, 4C06E5C220EC553A0055D09A /* IRCAddressBookMatchCache.m in Sources */, 4C06E5C320EC553A0055D09A /* IRCAddressBookUserTracking.m in Sources */, 4C06E5C420EC553A0055D09A /* IRCChannel.m in Sources */, 4C06E5C520EC553A0055D09A /* IRCChannelConfig.m in Sources */, 4C06E5C620EC553A0055D09A /* IRCChannelMode.m in Sources */, 4C06E5C720EC553A0055D09A /* IRCChannelUser.m in Sources */, 4C06E5C820EC553A0055D09A /* IRCClient.m in Sources */, 4C06E5C920EC553A0055D09A /* IRCClientConfig.m in Sources */, 4C06E5CA20EC553A0055D09A /* IRCClientRequestedCommands.m in Sources */, 4C06E5CB20EC553A0055D09A /* IRCCommandIndex.m in Sources */, 4C06E5CC20EC553A0055D09A /* IRCConnection.m in Sources */, 4C06E5CD20EC553A0055D09A /* IRCConnectionConfig.m in Sources */, 4C06E5CE20EC553A0055D09A /* IRCExtras.m in Sources */, 4C06E5CF20EC553A0055D09A /* IRCHighlightLogEntry.m in Sources */, 4C06E5D020EC553A0055D09A /* IRCHighlightMatchCondition.m in Sources */, 4C469E3B20EC5BDB00094EA4 /* TLOLinkParser.swift in Sources */, 4C06E5D120EC553A0055D09A /* IRCISupportInfo.m in Sources */, 4C06E5D220EC553A0055D09A /* IRCMessage.m in Sources */, 4C06E5D320EC553A0055D09A /* IRCMessageBatch.m in Sources */, 4C06E5D420EC553A0055D09A /* IRCModeInfo.m in Sources */, 4C06E5D520EC553A0055D09A /* IRCNetworkList.m in Sources */, 4C06E5D620EC553A0055D09A /* IRCPrefix.m in Sources */, 4C51327920F76E980033B703 /* TLOLocalization.swift in Sources */, 4C06E5D720EC553A0055D09A /* IRCSendingMessage.m in Sources */, 4C06E5D820EC553A0055D09A /* IRCServer.m in Sources */, 4C06E5D920EC553A0055D09A /* IRCTimerCommand.m in Sources */, 4C8878052C33706E0016DB98 /* TPCSandboxMigration.m in Sources */, 4C06E5DA20EC553A0055D09A /* IRCTreeItem.m in Sources */, 4C06E5DB20EC553A0055D09A /* IRCWorld.m in Sources */, 4C06E5DD20EC553A0055D09A /* IRCUserRelations.m in Sources */, 4C06E5DE20EC553A0055D09A /* IRCUser.m in Sources */, 4C06E5DF20EC553A0055D09A /* IRCUserNicknameColorStyleGenerator.m in Sources */, 4C06E5E020EC553A0055D09A /* IRCUserPersistentStore.m in Sources */, 4C06E5E120EC553A0055D09A /* IRCColorFormat.m in Sources */, 4C06E5E220EC553A0055D09A /* OELReachability.m in Sources */, 4C06E5E320EC553A0055D09A /* TLOLicenseManager.m in Sources */, 4C06E5E420EC553A0055D09A /* TLOLicenseManagerDownloader.m in Sources */, 4C06E5E520EC553A0055D09A /* TLOLicenseManagerLastGen.m in Sources */, 4C06E5E720EC553A0055D09A /* TLOEncryptionManager.m in Sources */, 4C06E5E820EC553A0055D09A /* TLOFileLogger.m in Sources */, 4C06E5E920EC553A0055D09A /* TLONotificationController.m in Sources */, 4C06E5EA20EC553A0055D09A /* TLOInputHistory.m in Sources */, 4C06E5EB20EC553A0055D09A /* TLOInternetAddressLookup.m in Sources */, 4C06E5EC20EC553A0055D09A /* TLOKeyEventHandler.m in Sources */, 4C06E5ED20EC553A0055D09A /* TLOLocalization.m in Sources */, 4C06E5EE20EC553A0055D09A /* TLONicknameCompletionStatus.m in Sources */, 4C06E5EF20EC553A0055D09A /* TLONotificationConfiguration.m in Sources */, 4C06E5F120EC553A0055D09A /* TLOSoundPlayer.m in Sources */, 4C06E5F220EC553A0055D09A /* TLOSpeechSynthesizer.m in Sources */, 4C06E5F320EC553A0055D09A /* TLOSpokenNotification.m in Sources */, 4C06E5F420EC553A0055D09A /* TLOTimer.m in Sources */, 4C06E5F520EC553A0055D09A /* main.m in Sources */, 4C06E5F820EC553A0055D09A /* TPCThemeController.m in Sources */, 4C06E5F920EC553A0055D09A /* TPCTheme.m in Sources */, 4C06E5FA20EC553A0055D09A /* TPCApplicationInfo.m in Sources */, 4C06E5FB20EC553A0055D09A /* TPCPathInfo.m in Sources */, 4C469E3C20EC5BDB00094EA4 /* TLOpenLink.swift in Sources */, 4C06E5FC20EC553A0055D09A /* TPCPreferences.m in Sources */, 4C06E5FD20EC553A0055D09A /* TPCPreferencesLocal.m in Sources */, 4C06E5FE20EC553A0055D09A /* TPCPreferencesImportExport.m in Sources */, 4C06E5FF20EC553A0055D09A /* TPCPreferencesReload.m in Sources */, 4C06E60020EC553A0055D09A /* TPCPreferencesUserDefaults.m in Sources */, 4C06E60120EC553A0055D09A /* TPCPreferencesUserDefaultsLocal.m in Sources */, 4C06E60320EC553A0055D09A /* TPCResourceManager.m in Sources */, 4C06E60420EC553A0055D09A /* ICLPayloadLocal.m in Sources */, 4C06E60620EC553A0055D09A /* GCDAsyncSocket.m in Sources */, 4C06E60720EC553A0055D09A /* GCDAsyncSocketExtensions.m in Sources */, 4C06E60920EC553A0055D09A /* ICLPayload.m in Sources */, 4C06E60A20EC553A0055D09A /* TVCLogLineXPC.m in Sources */, 4C06E60B20EC553A0055D09A /* TVCChannelSelectionOutlineCellView.m in Sources */, 4C8F3FDD2C31B15B00118AAF /* THOPluginItemLogging.m in Sources */, 4C06E60C20EC553A0055D09A /* TVCChannelSelectionViewController.m in Sources */, 4C06E60D20EC553A0055D09A /* TVCLogControllerHistoricLogFile.m in Sources */, 4C06E60E20EC553A0055D09A /* TVCLogControllerInlineMediaService.m in Sources */, 4C06E60F20EC553A0055D09A /* TVCLogControllerOperationQueue.m in Sources */, 4C06E61020EC553A0055D09A /* TVCLogController.m in Sources */, 4C06E61120EC553A0055D09A /* TVCLogLine.m in Sources */, 4C06E61220EC553A0055D09A /* TVCLogPolicy.m in Sources */, 4C06E61320EC553A0055D09A /* TVCLogRenderer.m in Sources */, 4C06E61420EC553A0055D09A /* TVCLogScriptEventSink.m in Sources */, 4C06E61520EC553A0055D09A /* TVCLogView.m in Sources */, 4C06E61620EC553A0055D09A /* TVCLogViewInternalWK1.m in Sources */, 4C06E61720EC553A0055D09A /* TVCLogViewInternalWK2.m in Sources */, 4C06E61820EC553A0055D09A /* TVCWK1AutoScroller.m in Sources */, 4C06E61920EC553A0055D09A /* TVCAlert.m in Sources */, 4C06E61A20EC553A0055D09A /* TVCErrorMessagePopover.m in Sources */, 4C06E61B20EC553A0055D09A /* TVCErrorMessagePopoverController.m in Sources */, 4C06E61C20EC553A0055D09A /* TVCAutoExpandingTextField.m in Sources */, 4C06E61D20EC553A0055D09A /* TVCAutoExpandingTokenField.m in Sources */, 4C06E61E20EC553A0055D09A /* TVCTextFormatterMenu.m in Sources */, 4C06E61F20EC553A0055D09A /* TVCTextViewWithIRCFormatter.m in Sources */, 4C06E62020EC553A0055D09A /* TVCValidatedComboBox.m in Sources */, 4C06E62120EC553A0055D09A /* TVCValidatedTextField.m in Sources */, 4C06E62220EC553A0055D09A /* TVCMainWindow.m in Sources */, 4C06E62320EC553A0055D09A /* TVCMainWindowAppearance.m in Sources */, 4C06E62420EC553A0055D09A /* TVCMainWindowChannelView.m in Sources */, 4C06E62520EC553A0055D09A /* TVCMainWindowLoadingScreen.m in Sources */, 4C06E62620EC553A0055D09A /* TVCMainWindowSegmentedControl.m in Sources */, 4C06E62820EC553A0055D09A /* TVCMainWindowSplitView.m in Sources */, 4C06E62920EC553A0055D09A /* TVCMainWindowTextView.m in Sources */, 4C06E62A20EC553A0055D09A /* TVCMainWindowTextViewAppearance.m in Sources */, 4C06E62B20EC553A0055D09A /* TVCMainWindowTitlebarAccessoryView.m in Sources */, 4C06E62C20EC553A0055D09A /* TVCNotificationConfigurationViewController.m in Sources */, 4C06E62D20EC553A0055D09A /* TVCServerList.m in Sources */, 4C5274E720F8D7CD00B18F9D /* IRCConnectionErrors.m in Sources */, 4C06E62E20EC553A0055D09A /* TVCServerListAppearance.m in Sources */, 4C06E62F20EC553A0055D09A /* TVCServerListCell.m in Sources */, 4C06E63020EC553A0055D09A /* TVCMemberList.m in Sources */, 4C06E63120EC553A0055D09A /* TVCMemberListAppearance.m in Sources */, 4C06E63220EC553A0055D09A /* TVCMemberListCell.m in Sources */, 4C06E63320EC553A0055D09A /* TVCMemberListUserInfoPopover.m in Sources */, 4C06E63420EC553A0055D09A /* TVCAppearance.m in Sources */, 4C06E63520EC553A0055D09A /* TVCBasicTableView.m in Sources */, 4C06E63620EC553A0055D09A /* TVCContentNavigationOutlineView.m in Sources */, 4CB873DD22B4A56B005AB046 /* TDCPreferencesUserStyleSheet.m in Sources */, 4C06E63720EC553A0055D09A /* TVCDockIcon.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 4C1DC54E1580420500A47BC9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C06E63820EC55B90055D09A /* TXAppearance.m in Sources */, 4C06E63920EC55B90055D09A /* TXApplication.m in Sources */, 4C06E63A20EC55B90055D09A /* TXGlobalModels.m in Sources */, 4C06E63B20EC55B90055D09A /* TXMasterController.m in Sources */, 4C06E63C20EC55B90055D09A /* TXMenuController.m in Sources */, 4C06E63D20EC55B90055D09A /* TXSharedApplication.m in Sources */, 4C06E63E20EC55B90055D09A /* TXWindowController.m in Sources */, 4C06E64320EC55B90055D09A /* TDCChannelSpotlightControls.m in Sources */, 4C06E64420EC55B90055D09A /* TDCChannelSpotlightSearchResult.m in Sources */, 4C06E64520EC55B90055D09A /* TDCChannelSpotlightController.m in Sources */, 4C06E64620EC55B90055D09A /* TDCChannelSpotlightAppearance.m in Sources */, 4C06E64720EC55B90055D09A /* TDCChannelSpotlightSearchResultsTable.m in Sources */, 4C06E64820EC55B90055D09A /* TDCFileTransferDialog.m in Sources */, 4C06E64920EC55B90055D09A /* TDCFileTransferDialogTransferController.m in Sources */, 4C06E64A20EC55B90055D09A /* TDCFileTransferDialogTableCell.m in Sources */, 4C06E64B20EC55B90055D09A /* TDCLicenseUpgradeDialog.m in Sources */, 4CAB27532537EE44009B1F07 /* IRCChannelMemberList.m in Sources */, 4C06E64C20EC55B90055D09A /* TDCLicenseUpgradeEligibilitySheet.m in Sources */, 4C06E64D20EC55B90055D09A /* TDCLicenseManagerMigrateAppStoreSheet.m in Sources */, 4C06E64E20EC55B90055D09A /* TDCLicenseUpgradeCommonActions.m in Sources */, 4C06E64F20EC55B90055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.m in Sources */, 4C06E65020EC55B90055D09A /* TDCLicenseUpgradeActivateSheet.m in Sources */, 4CAB2767253863BF009B1F07 /* IRCChannelMemberListController.m in Sources */, 4C06E65120EC55B90055D09A /* TDCLicenseManagerDialog.m in Sources */, 4C06E65620EC55B90055D09A /* TDCPreferencesNotificationConfiguration.m in Sources */, 4C06E65720EC55B90055D09A /* TDCPreferencesController.m in Sources */, 4C06E65820EC55B90055D09A /* TDCServerEndpointListSheetTable.m in Sources */, 4C06E65920EC55B90055D09A /* TDCServerEndpointListSheet.m in Sources */, 4C06E65A20EC55B90055D09A /* TDCAboutDialog.m in Sources */, 4C06E65B20EC55B90055D09A /* TDCAddressBookSheet.m in Sources */, 4C06E65C20EC55B90055D09A /* TDCAlert.m in Sources */, 4C06E65D20EC55B90055D09A /* TDCChannelBanListSheet.m in Sources */, 4C06E65E20EC55B90055D09A /* TDCChannelInviteSheet.m in Sources */, 4C06E65F20EC55B90055D09A /* TDCChannelModifyModesSheet.m in Sources */, 4C06E66020EC55B90055D09A /* TDCChannelModifyTopicSheet.m in Sources */, 4C06E66120EC55B90055D09A /* TDCChannelPropertiesNotificationConfiguration.m in Sources */, 4C06E66220EC55B90055D09A /* TDCChannelPropertiesSheet.m in Sources */, 4C06E66320EC55B90055D09A /* TDCHighlightEntrySheet.m in Sources */, 4C06E66420EC55B90055D09A /* TDCInputPrompt.m in Sources */, 4C06E66520EC55B90055D09A /* TDCNicknameColorSheet.m in Sources */, 4C06E66620EC55B90055D09A /* TDCProgressIndicatorSheet.m in Sources */, 4C06E66720EC55B90055D09A /* TDCServerChangeNicknameSheet.m in Sources */, 4C06E66820EC55B90055D09A /* TDCServerChannelListDialog.m in Sources */, 4C06E66920EC55B90055D09A /* TDCServerHighlightListSheet.m in Sources */, 4C06E66A20EC55B90055D09A /* TDCServerPropertiesSheet.m in Sources */, 4C06E66B20EC55B90055D09A /* TDCSheetBase.m in Sources */, 4C06E66C20EC55B90055D09A /* TDCWelcomeSheet.m in Sources */, 4C06E66D20EC55B90055D09A /* TDCWindowBase.m in Sources */, 4C06E66E20EC55B90055D09A /* NSColorHelper.m in Sources */, 4C06E66F20EC55B90055D09A /* NSObjectHelper.m in Sources */, 4C06E67020EC55B90055D09A /* NSStringHelper.m in Sources */, 4C06E67120EC55B90055D09A /* NSTableViewHelper.m in Sources */, 4C06E67220EC55B90055D09A /* NSViewHelper.m in Sources */, 4C06E67320EC55B90055D09A /* TXAppearanceHelper.m in Sources */, 4C06E67420EC55B90055D09A /* WebScriptObjectHelper.m in Sources */, 4C06E67520EC55B90055D09A /* GTMEncodeHTML.m in Sources */, 4C06E67620EC55B90055D09A /* THOPluginDispatcher.m in Sources */, 4C06E67720EC55B90055D09A /* THOPluginManager.m in Sources */, 4C06E67820EC55B90055D09A /* THOPluginItem.m in Sources */, 4C06E67920EC55B90055D09A /* THOUnicodeHelper.m in Sources */, 4C06E67A20EC55B90055D09A /* IRCAddressBook.m in Sources */, 4C06E67B20EC55B90055D09A /* IRCAddressBookMatchCache.m in Sources */, 4C06E67C20EC55B90055D09A /* IRCAddressBookUserTracking.m in Sources */, 4C06E67D20EC55B90055D09A /* IRCChannel.m in Sources */, 4C06E67E20EC55B90055D09A /* IRCChannelConfig.m in Sources */, 4C06E67F20EC55B90055D09A /* IRCChannelMode.m in Sources */, 4C06E68020EC55B90055D09A /* IRCChannelUser.m in Sources */, 4C06E68120EC55B90055D09A /* IRCClient.m in Sources */, 4C06E68220EC55B90055D09A /* IRCClientConfig.m in Sources */, 4C06E68320EC55B90055D09A /* IRCClientRequestedCommands.m in Sources */, 4C06E68420EC55B90055D09A /* IRCCommandIndex.m in Sources */, 4C06E68520EC55B90055D09A /* IRCConnection.m in Sources */, 4C06E68620EC55B90055D09A /* IRCConnectionConfig.m in Sources */, 4C06E68720EC55B90055D09A /* IRCExtras.m in Sources */, 4C06E68820EC55B90055D09A /* IRCHighlightLogEntry.m in Sources */, 4C06E68920EC55B90055D09A /* IRCHighlightMatchCondition.m in Sources */, 4C469E3D20EC5BDB00094EA4 /* TLOLinkParser.swift in Sources */, 4C06E68A20EC55B90055D09A /* IRCISupportInfo.m in Sources */, 4C06E68B20EC55B90055D09A /* IRCMessage.m in Sources */, 4C06E68C20EC55B90055D09A /* IRCMessageBatch.m in Sources */, 4C06E68D20EC55B90055D09A /* IRCModeInfo.m in Sources */, 4C06E68E20EC55B90055D09A /* IRCNetworkList.m in Sources */, 4C06E68F20EC55B90055D09A /* IRCPrefix.m in Sources */, 4C51327A20F76E980033B703 /* TLOLocalization.swift in Sources */, 4C06E69020EC55B90055D09A /* IRCSendingMessage.m in Sources */, 4C06E69120EC55B90055D09A /* IRCServer.m in Sources */, 4C06E69220EC55B90055D09A /* IRCTimerCommand.m in Sources */, 4C8878042C33706E0016DB98 /* TPCSandboxMigration.m in Sources */, 4C06E69320EC55B90055D09A /* IRCTreeItem.m in Sources */, 4C06E69420EC55B90055D09A /* IRCWorld.m in Sources */, 4C06E69620EC55B90055D09A /* IRCUserRelations.m in Sources */, 4C06E69720EC55B90055D09A /* IRCUser.m in Sources */, 4C06E69820EC55B90055D09A /* IRCUserNicknameColorStyleGenerator.m in Sources */, 4C06E69920EC55B90055D09A /* IRCUserPersistentStore.m in Sources */, 4C06E69A20EC55B90055D09A /* IRCColorFormat.m in Sources */, 4C06E69B20EC55B90055D09A /* OELReachability.m in Sources */, 4C06E69C20EC55B90055D09A /* TLOLicenseManager.m in Sources */, 4C06E69D20EC55B90055D09A /* TLOLicenseManagerDownloader.m in Sources */, 4C06E69E20EC55B90055D09A /* TLOLicenseManagerLastGen.m in Sources */, 4C06E6A020EC55B90055D09A /* TLOEncryptionManager.m in Sources */, 4C06E6A120EC55B90055D09A /* TLOFileLogger.m in Sources */, 4C06E6A220EC55B90055D09A /* TLONotificationController.m in Sources */, 4C06E6A320EC55B90055D09A /* TLOInputHistory.m in Sources */, 4C06E6A420EC55B90055D09A /* TLOInternetAddressLookup.m in Sources */, 4C06E6A520EC55B90055D09A /* TLOKeyEventHandler.m in Sources */, 4C06E6A620EC55B90055D09A /* TLOLocalization.m in Sources */, 4C06E6A720EC55B90055D09A /* TLONicknameCompletionStatus.m in Sources */, 4C06E6A820EC55B90055D09A /* TLONotificationConfiguration.m in Sources */, 4C06E6AA20EC55B90055D09A /* TLOSoundPlayer.m in Sources */, 4C06E6AB20EC55B90055D09A /* TLOSpeechSynthesizer.m in Sources */, 4C06E6AC20EC55B90055D09A /* TLOSpokenNotification.m in Sources */, 4C06E6AD20EC55B90055D09A /* TLOTimer.m in Sources */, 4C06E6AE20EC55B90055D09A /* main.m in Sources */, 4C06E6B120EC55B90055D09A /* TPCThemeController.m in Sources */, 4C06E6B220EC55B90055D09A /* TPCTheme.m in Sources */, 4C06E6B320EC55B90055D09A /* TPCApplicationInfo.m in Sources */, 4C06E6B420EC55B90055D09A /* TPCPathInfo.m in Sources */, 4C469E3E20EC5BDB00094EA4 /* TLOpenLink.swift in Sources */, 4C06E6B520EC55B90055D09A /* TPCPreferences.m in Sources */, 4C06E6B620EC55B90055D09A /* TPCPreferencesLocal.m in Sources */, 4C06E6B720EC55B90055D09A /* TPCPreferencesImportExport.m in Sources */, 4C06E6B820EC55B90055D09A /* TPCPreferencesReload.m in Sources */, 4C06E6B920EC55B90055D09A /* TPCPreferencesUserDefaults.m in Sources */, 4C06E6BA20EC55B90055D09A /* TPCPreferencesUserDefaultsLocal.m in Sources */, 4C06E6BC20EC55B90055D09A /* TPCResourceManager.m in Sources */, 4C06E6BD20EC55B90055D09A /* ICLPayloadLocal.m in Sources */, 4C06E6BF20EC55B90055D09A /* GCDAsyncSocket.m in Sources */, 4C06E6C020EC55B90055D09A /* GCDAsyncSocketExtensions.m in Sources */, 4C06E6C220EC55B90055D09A /* ICLPayload.m in Sources */, 4C06E6C320EC55B90055D09A /* TVCLogLineXPC.m in Sources */, 4C06E6C420EC55B90055D09A /* TVCChannelSelectionOutlineCellView.m in Sources */, 4C8F3FDC2C31B15B00118AAF /* THOPluginItemLogging.m in Sources */, 4C06E6C520EC55B90055D09A /* TVCChannelSelectionViewController.m in Sources */, 4C06E6C620EC55B90055D09A /* TVCLogControllerHistoricLogFile.m in Sources */, 4C06E6C720EC55B90055D09A /* TVCLogControllerInlineMediaService.m in Sources */, 4C06E6C820EC55B90055D09A /* TVCLogControllerOperationQueue.m in Sources */, 4C06E6C920EC55B90055D09A /* TVCLogController.m in Sources */, 4C06E6CA20EC55B90055D09A /* TVCLogLine.m in Sources */, 4C06E6CB20EC55B90055D09A /* TVCLogPolicy.m in Sources */, 4C06E6CC20EC55B90055D09A /* TVCLogRenderer.m in Sources */, 4C06E6CD20EC55B90055D09A /* TVCLogScriptEventSink.m in Sources */, 4C06E6CE20EC55B90055D09A /* TVCLogView.m in Sources */, 4C06E6CF20EC55B90055D09A /* TVCLogViewInternalWK1.m in Sources */, 4C06E6D020EC55B90055D09A /* TVCLogViewInternalWK2.m in Sources */, 4C06E6D120EC55B90055D09A /* TVCWK1AutoScroller.m in Sources */, 4C06E6D220EC55B90055D09A /* TVCAlert.m in Sources */, 4C06E6D320EC55B90055D09A /* TVCErrorMessagePopover.m in Sources */, 4C06E6D420EC55B90055D09A /* TVCErrorMessagePopoverController.m in Sources */, 4C06E6D520EC55B90055D09A /* TVCAutoExpandingTextField.m in Sources */, 4C06E6D620EC55B90055D09A /* TVCAutoExpandingTokenField.m in Sources */, 4C06E6D720EC55B90055D09A /* TVCTextFormatterMenu.m in Sources */, 4C06E6D820EC55B90055D09A /* TVCTextViewWithIRCFormatter.m in Sources */, 4C06E6D920EC55B90055D09A /* TVCValidatedComboBox.m in Sources */, 4C06E6DA20EC55B90055D09A /* TVCValidatedTextField.m in Sources */, 4C06E6DB20EC55B90055D09A /* TVCMainWindow.m in Sources */, 4C06E6DC20EC55B90055D09A /* TVCMainWindowAppearance.m in Sources */, 4C06E6DD20EC55B90055D09A /* TVCMainWindowChannelView.m in Sources */, 4C06E6DE20EC55B90055D09A /* TVCMainWindowLoadingScreen.m in Sources */, 4C06E6DF20EC55B90055D09A /* TVCMainWindowSegmentedControl.m in Sources */, 4C06E6E120EC55B90055D09A /* TVCMainWindowSplitView.m in Sources */, 4C06E6E220EC55B90055D09A /* TVCMainWindowTextView.m in Sources */, 4C06E6E320EC55B90055D09A /* TVCMainWindowTextViewAppearance.m in Sources */, 4C06E6E420EC55B90055D09A /* TVCMainWindowTitlebarAccessoryView.m in Sources */, 4C06E6E520EC55B90055D09A /* TVCNotificationConfigurationViewController.m in Sources */, 4C06E6E620EC55B90055D09A /* TVCServerList.m in Sources */, 4C5274E820F8D7CD00B18F9D /* IRCConnectionErrors.m in Sources */, 4C06E6E720EC55B90055D09A /* TVCServerListAppearance.m in Sources */, 4C06E6E820EC55B90055D09A /* TVCServerListCell.m in Sources */, 4C06E6E920EC55B90055D09A /* TVCMemberList.m in Sources */, 4C06E6EA20EC55B90055D09A /* TVCMemberListAppearance.m in Sources */, 4C06E6EB20EC55B90055D09A /* TVCMemberListCell.m in Sources */, 4C06E6EC20EC55B90055D09A /* TVCMemberListUserInfoPopover.m in Sources */, 4C06E6ED20EC55B90055D09A /* TVCAppearance.m in Sources */, 4C06E6EE20EC55B90055D09A /* TVCBasicTableView.m in Sources */, 4C06E6EF20EC55B90055D09A /* TVCContentNavigationOutlineView.m in Sources */, 4CB873DE22B4A56B005AB046 /* TDCPreferencesUserStyleSheet.m in Sources */, 4C06E6F020EC55B90055D09A /* TVCDockIcon.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 4CBB842D16F0DA1D004E3ED6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4CCF301015804DCE006FFE21 /* Build Frameworks */; targetProxy = 4CBB842C16F0DA1D004E3ED6 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 4C06E13920EC4AC40055D09A /* TDCServerPropertiesSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E13A20EC4AC40055D09A /* en */, ); name = TDCServerPropertiesSheet.xib; sourceTree = ""; }; 4C06E13D20EC4AC40055D09A /* TDCServerHighlightListSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E13E20EC4AC40055D09A /* en */, ); name = TDCServerHighlightListSheet.xib; sourceTree = ""; }; 4C06E13F20EC4AC40055D09A /* TDCServerEndpointListSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E14020EC4AC40055D09A /* en */, ); name = TDCServerEndpointListSheet.xib; sourceTree = ""; }; 4C06E14120EC4AC40055D09A /* TDCChannelModifyModesSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E14220EC4AC40055D09A /* en */, ); name = TDCChannelModifyModesSheet.xib; sourceTree = ""; }; 4C06E14320EC4AC40055D09A /* TDCChannelSpotlightController.xib */ = { isa = PBXVariantGroup; children = ( 4C06E14420EC4AC40055D09A /* en */, ); name = TDCChannelSpotlightController.xib; sourceTree = ""; }; 4C06E14520EC4AC40055D09A /* TDCFileTransferDialog.xib */ = { isa = PBXVariantGroup; children = ( 4C06E14620EC4AC40055D09A /* en */, ); name = TDCFileTransferDialog.xib; sourceTree = ""; }; 4C06E14720EC4AC40055D09A /* TDCServerChannelListDialog.xib */ = { isa = PBXVariantGroup; children = ( 4C06E14820EC4AC40055D09A /* en */, ); name = TDCServerChannelListDialog.xib; sourceTree = ""; }; 4C06E14920EC4AC40055D09A /* TDCChannelBanListSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E14A20EC4AC40055D09A /* en */, ); name = TDCChannelBanListSheet.xib; sourceTree = ""; }; 4C06E14B20EC4AC40055D09A /* TDCChannelPropertiesSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E14C20EC4AC40055D09A /* en */, ); name = TDCChannelPropertiesSheet.xib; sourceTree = ""; }; 4C06E14D20EC4AC40055D09A /* TDCLicenseManagerMigrateAppStoreSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E14E20EC4AC40055D09A /* en */, ); name = TDCLicenseManagerMigrateAppStoreSheet.xib; sourceTree = ""; }; 4C06E15120EC4AC40055D09A /* TDCLicenseUpgradeEligibilitySheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E15220EC4AC40055D09A /* en */, ); name = TDCLicenseUpgradeEligibilitySheet.xib; sourceTree = ""; }; 4C06E15520EC4AC40055D09A /* TVCChannelSelectionView.xib */ = { isa = PBXVariantGroup; children = ( 4C06E15620EC4AC40055D09A /* en */, ); name = TVCChannelSelectionView.xib; sourceTree = ""; }; 4C06E15720EC4AC40055D09A /* TXCMainMenu.xib */ = { isa = PBXVariantGroup; children = ( 4C06E15820EC4AC40055D09A /* en */, ); name = TXCMainMenu.xib; sourceTree = ""; }; 4C06E15920EC4AC40055D09A /* TVCNotificationConfigurationView.xib */ = { isa = PBXVariantGroup; children = ( 4C06E15A20EC4AC40055D09A /* en */, ); name = TVCNotificationConfigurationView.xib; sourceTree = ""; }; 4C06E15B20EC4AC40055D09A /* TDCLicenseUpgradeActivateSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E15C20EC4AC40055D09A /* en */, ); name = TDCLicenseUpgradeActivateSheet.xib; sourceTree = ""; }; 4C06E15D20EC4AC40055D09A /* TDCPreferences.xib */ = { isa = PBXVariantGroup; children = ( 4C06E15E20EC4AC40055D09A /* en */, ); name = TDCPreferences.xib; sourceTree = ""; }; 4C06E15F20EC4AC40055D09A /* TDCChannelInviteSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E16020EC4AC40055D09A /* en */, ); name = TDCChannelInviteSheet.xib; sourceTree = ""; }; 4C06E16120EC4AC40055D09A /* TDCNicknameColorSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E16220EC4AC40055D09A /* en */, ); name = TDCNicknameColorSheet.xib; sourceTree = ""; }; 4C06E16320EC4AC40055D09A /* TDCWelcomeSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E16420EC4AC40055D09A /* en */, ); name = TDCWelcomeSheet.xib; sourceTree = ""; }; 4C06E16520EC4AC40055D09A /* TDCProgressIndicatorSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E16620EC4AC40055D09A /* en */, ); name = TDCProgressIndicatorSheet.xib; sourceTree = ""; }; 4C06E16720EC4AC40055D09A /* TDCAddressBookSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E16820EC4AC40055D09A /* en */, ); name = TDCAddressBookSheet.xib; sourceTree = ""; }; 4C06E16920EC4AC40055D09A /* TVCAlert.xib */ = { isa = PBXVariantGroup; children = ( 4C06E16A20EC4AC40055D09A /* en */, ); name = TVCAlert.xib; sourceTree = ""; }; 4C06E16B20EC4AC40055D09A /* TDCLicenseUpgradeDialog.xib */ = { isa = PBXVariantGroup; children = ( 4C06E16C20EC4AC40055D09A /* en */, ); name = TDCLicenseUpgradeDialog.xib; sourceTree = ""; }; 4C06E16D20EC4AC40055D09A /* TDCServerChangeNicknameSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E16E20EC4AC40055D09A /* en */, ); name = TDCServerChangeNicknameSheet.xib; sourceTree = ""; }; 4C06E16F20EC4AC40055D09A /* TDCChannelModifyTopicSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E17020EC4AC40055D09A /* en */, ); name = TDCChannelModifyTopicSheet.xib; sourceTree = ""; }; 4C06E17120EC4AC40055D09A /* TDCAboutDialog.xib */ = { isa = PBXVariantGroup; children = ( 4C06E17220EC4AC40055D09A /* en */, ); name = TDCAboutDialog.xib; sourceTree = ""; }; 4C06E17320EC4AC40055D09A /* TDCHighlightEntrySheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E17420EC4AC40055D09A /* en */, ); name = TDCHighlightEntrySheet.xib; sourceTree = ""; }; 4C06E17520EC4AC40055D09A /* TVCMainWindow.xib */ = { isa = PBXVariantGroup; children = ( 4C06E17620EC4AC40055D09A /* en */, ); name = TVCMainWindow.xib; sourceTree = ""; }; 4C06E17720EC4AC40055D09A /* TDCLicenseManagerDialog.xib */ = { isa = PBXVariantGroup; children = ( 4C06E17820EC4AC40055D09A /* en */, ); name = TDCLicenseManagerDialog.xib; sourceTree = ""; }; 4C06E17920EC4AC40055D09A /* TDCLicenseManagerRecoverLostLicenseSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C06E17A20EC4AC40055D09A /* en */, ); name = TDCLicenseManagerRecoverLostLicenseSheet.xib; sourceTree = ""; }; 4C06E2A720EC4FF00055D09A /* Prompts.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2A820EC4FF00055D09A /* en */, ); name = Prompts.strings; sourceTree = ""; }; 4C06E2A920EC4FF00055D09A /* OffTheRecord.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2AA20EC4FF00055D09A /* en */, ); name = OffTheRecord.strings; sourceTree = ""; }; 4C06E2AB20EC4FF00055D09A /* CommonErrors.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2AC20EC4FF00055D09A /* en */, ); name = CommonErrors.strings; sourceTree = ""; }; 4C06E2AF20EC4FF00055D09A /* BasicLanguage.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2B020EC4FF00055D09A /* en */, ); name = BasicLanguage.strings; sourceTree = ""; }; 4C06E2B120EC4FF00055D09A /* Accessibility.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2B220EC4FF00055D09A /* en */, ); name = Accessibility.strings; sourceTree = ""; }; 4C06E2B320EC4FF00055D09A /* TDCChannelPropertiesSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2B420EC4FF00055D09A /* en */, ); name = TDCChannelPropertiesSheet.strings; sourceTree = ""; }; 4C06E2B520EC4FF00055D09A /* TDCChannelBanListSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2B620EC4FF00055D09A /* en */, ); name = TDCChannelBanListSheet.strings; sourceTree = ""; }; 4C06E2B720EC4FF00055D09A /* TDCPreferencesController.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2B820EC4FF00055D09A /* en */, ); name = TDCPreferencesController.strings; sourceTree = ""; }; 4C06E2B920EC4FF00055D09A /* TDCFileTransferDialog.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2BA20EC4FF00055D09A /* en */, ); name = TDCFileTransferDialog.strings; sourceTree = ""; }; 4C06E2BB20EC4FF00055D09A /* TDCChannelInviteSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2BC20EC4FF00055D09A /* en */, ); name = TDCChannelInviteSheet.strings; sourceTree = ""; }; 4C06E2BD20EC4FF00055D09A /* TDCAboutDialog.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2BE20EC4FF00055D09A /* en */, ); name = TDCAboutDialog.strings; sourceTree = ""; }; 4C06E2BF20EC4FF00055D09A /* TDCServerPropertiesSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2C020EC4FF00055D09A /* en */, ); name = TDCServerPropertiesSheet.strings; sourceTree = ""; }; 4C06E2C120EC4FF00055D09A /* IRC.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2C220EC4FF00055D09A /* en */, ); name = IRC.strings; sourceTree = ""; }; 4C06E2C320EC4FF00055D09A /* TLOLicenseManager.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2C420EC4FF00055D09A /* en */, ); name = TLOLicenseManager.strings; sourceTree = ""; }; 4C06E2C520EC4FF00055D09A /* TDCChannelModifyModesSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2C620EC4FF00055D09A /* en */, ); name = TDCChannelModifyModesSheet.strings; sourceTree = ""; }; 4C06E2C720EC4FF00055D09A /* TVCMainWindow.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2C820EC4FF00055D09A /* en */, ); name = TVCMainWindow.strings; sourceTree = ""; }; 4C06E2C920EC4FF00055D09A /* Notifications.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2CA20EC4FF00055D09A /* en */, ); name = Notifications.strings; sourceTree = ""; }; 4C06E2CD20EC4FF00055D09A /* TVCNotificationConfigurationView.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2CE20EC4FF00055D09A /* en */, ); name = TVCNotificationConfigurationView.strings; sourceTree = ""; }; 4C06E2CF20EC4FF00055D09A /* TDCServerEndpointListSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2D020EC4FF00055D09A /* en */, ); name = TDCServerEndpointListSheet.strings; sourceTree = ""; }; 4C06E2D120EC4FF00055D09A /* TDCServerChannelListDialog.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2D220EC4FF00055D09A /* en */, ); name = TDCServerChannelListDialog.strings; sourceTree = ""; }; 4C06E2D320EC4FF00055D09A /* TDCChannelSpotlightController.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2D420EC4FF00055D09A /* en */, ); name = TDCChannelSpotlightController.strings; sourceTree = ""; }; 4C06E2D520EC4FF00055D09A /* TDCAddressBookSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2D620EC4FF00055D09A /* en */, ); name = TDCAddressBookSheet.strings; sourceTree = ""; }; 4C06E2D720EC4FF00055D09A /* TDCLicenseUpgradeEligibilitySheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2D820EC4FF00055D09A /* en */, ); name = TDCLicenseUpgradeEligibilitySheet.strings; sourceTree = ""; }; 4C06E2D920EC4FF00055D09A /* TDCChannelModifyTopicSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C06E2DA20EC4FF00055D09A /* en */, ); name = TDCChannelModifyTopicSheet.strings; sourceTree = ""; }; 4C17305222B4A94000DC5836 /* TDCPreferencesUserStyleSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C17305322B4A94000DC5836 /* en */, ); name = TDCPreferencesUserStyleSheet.xib; sourceTree = ""; }; 4CEFEDAE22B4B6E8002CEE19 /* TDCPreferencesUserStyleSheet.strings */ = { isa = PBXVariantGroup; children = ( 4CEFEDAF22B4B6E8002CEE19 /* en */, ); name = TDCPreferencesUserStyleSheet.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 4C13442320C75C4900343AB7 /* Release (Sandboxed) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (Sandboxed)"; }; 4C13442520C75C4900343AB7 /* Release (Sandboxed) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (Sandboxed)"; }; 4C13442620C75C4900343AB7 /* Release (Sandboxed) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (Sandboxed)"; }; 4C13442820C75C4900343AB7 /* Release (Sandboxed) */ = { isa = XCBuildConfiguration; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; }; name = "Release (Sandboxed)"; }; 4C1DC56E1580420500A47BC9 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C3154DF20EB685700448776 /* Textual App.xcconfig */; buildSettings = { }; name = Debug; }; 4C1DC56F1580420500A47BC9 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C3154B720EB685700448776 /* Textual App.xcconfig */; buildSettings = { }; name = Release; }; 4C1DC5711580420500A47BC9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; 4C1DC5721580420500A47BC9 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; 4C5BA4E216F1302F00A96CA2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; 4C5BA4E316F1302F00A96CA2 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; 4CCF301215804DCE006FFE21 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 4CCF301315804DCE006FFE21 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 4C1DC54C1580420500A47BC9 /* Build configuration list for PBXProject "Textual App" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C1DC56E1580420500A47BC9 /* Debug */, 4C1DC56F1580420500A47BC9 /* Release */, 4C13442320C75C4900343AB7 /* Release (Sandboxed) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4C1DC5701580420500A47BC9 /* Build configuration list for PBXNativeTarget "Textual (Standard Release)" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C1DC5711580420500A47BC9 /* Debug */, 4C1DC5721580420500A47BC9 /* Release */, 4C13442620C75C4900343AB7 /* Release (Sandboxed) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4C5BA4E116F1302F00A96CA2 /* Build configuration list for PBXNativeTarget "Textual (Debug)" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C5BA4E216F1302F00A96CA2 /* Debug */, 4C5BA4E316F1302F00A96CA2 /* Release */, 4C13442520C75C4900343AB7 /* Release (Sandboxed) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4CCF301115804DCE006FFE21 /* Build configuration list for PBXAggregateTarget "Build Frameworks" */ = { isa = XCConfigurationList; buildConfigurations = ( 4CCF301215804DCE006FFE21 /* Debug */, 4CCF301315804DCE006FFE21 /* Release */, 4C13442820C75C4900343AB7 /* Release (Sandboxed) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 4C1DC5491580420500A47BC9 /* Project object */; } ================================================ FILE: Sources/App/Textual App.xcodeproj/xcshareddata/xcschemes/Textual (App Store).xcscheme ================================================ ================================================ FILE: Sources/App/Textual App.xcodeproj/xcshareddata/xcschemes/Textual (Debug).xcscheme ================================================ ================================================ FILE: Sources/App/Textual App.xcodeproj/xcshareddata/xcschemes/Textual (Standard Release Sandboxed).xcscheme ================================================ ================================================ FILE: Sources/App/Textual App.xcodeproj/xcshareddata/xcschemes/Textual (Standard Release).xcscheme ================================================ ================================================ FILE: Sources/Plugins/Blowfish Encryption/ACKNOWLEDGEMENT.txt ================================================ |----------------------------------------------------------------------------| | | | Portions of the “Blowfish Encryption Extension” project may utilize | | copyrighted work derived from various 3rd party, open source projects | | The use of this work is hereby acknowledged. | | | |----------------------------------------------------------------------------| Classes/BlowfishEncryptionKeyExchange.mm Classes/BlowfishEncryptionKeyExchange.h // Copyright (c) 2005-2013 Mathias Karlsson // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // Please see LICENSE-GPLv2.txt for further information. |-----------------------------------------------------------------------| ================================================ FILE: Sources/Plugins/Blowfish Encryption/Blowfish Encryption Extension.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 4C36AAFD20F27F8E007CA939 /* TPIBlowfishEncryption.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAFB20F27F8E007CA939 /* TPIBlowfishEncryption.xib */; }; 4C36AB0020F27F95007CA939 /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAFE20F27F95007CA939 /* BasicLanguage.strings */; }; 4C46A1BF20EC705F00094EA4 /* BlowfishEncryptionBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A1B520EC705F00094EA4 /* BlowfishEncryptionBase.m */; }; 4C46A1C020EC705F00094EA4 /* TPIBlowfishEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A1B620EC705F00094EA4 /* TPIBlowfishEncryption.m */; }; 4C46A1C120EC705F00094EA4 /* BlowfishEncryptionKeyExchange.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A1B720EC705F00094EA4 /* BlowfishEncryptionKeyExchange.mm */; }; 4C46A1C220EC705F00094EA4 /* BlowfishEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A1B820EC705F00094EA4 /* BlowfishEncryption.m */; }; 4C46A1C320EC705F00094EA4 /* TPIBlowfishEncryptionSwizzledClasses.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A1BB20EC705F00094EA4 /* TPIBlowfishEncryptionSwizzledClasses.m */; }; 4C46A1C420EC705F00094EA4 /* NSDataHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A1BC20EC705F00094EA4 /* NSDataHelper.m */; }; 4C46A1C520EC705F00094EA4 /* BlowfishEncryptionKeyExchangeBase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A1BD20EC705F00094EA4 /* BlowfishEncryptionKeyExchangeBase.mm */; }; 4C46A1D420EC70B600094EA4 /* LICENSE-GPLv2.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4C46A1D120EC70B600094EA4 /* LICENSE-GPLv2.txt */; }; 4C46A1D520EC70B600094EA4 /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4C46A1D220EC70B600094EA4 /* LICENSE.txt */; }; 4C46A1D620EC70B600094EA4 /* ACKNOWLEDGEMENT.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4C46A1D320EC70B600094EA4 /* ACKNOWLEDGEMENT.txt */; }; 4CBE7B4C1A91471A008FB230 /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CBE7B4A1A91471A008FB230 /* CocoaExtensions.framework */; }; 4CF284A01F8EEA0E00C14E24 /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF2849F1F8EE9BA00C14E24 /* libcrypto.a */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C36AAFC20F27F8E007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TPIBlowfishEncryption.xib; sourceTree = ""; }; 4C36AAFF20F27F95007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BasicLanguage.strings; sourceTree = ""; }; 4C46A1AF20EC705F00094EA4 /* TPIBlowfishEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPIBlowfishEncryption.h; sourceTree = ""; }; 4C46A1B120EC705F00094EA4 /* BlowfishEncryptionKeyExchangeBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlowfishEncryptionKeyExchangeBase.h; sourceTree = ""; }; 4C46A1B220EC705F00094EA4 /* BlowfishEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlowfishEncryption.h; sourceTree = ""; }; 4C46A1B320EC705F00094EA4 /* NSDataHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSDataHelper.h; sourceTree = ""; }; 4C46A1B420EC705F00094EA4 /* TPIBlowfishEncryptionSwizzledClasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPIBlowfishEncryptionSwizzledClasses.h; sourceTree = ""; }; 4C46A1B520EC705F00094EA4 /* BlowfishEncryptionBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlowfishEncryptionBase.m; sourceTree = ""; }; 4C46A1B620EC705F00094EA4 /* TPIBlowfishEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPIBlowfishEncryption.m; sourceTree = ""; }; 4C46A1B720EC705F00094EA4 /* BlowfishEncryptionKeyExchange.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BlowfishEncryptionKeyExchange.mm; sourceTree = ""; }; 4C46A1B820EC705F00094EA4 /* BlowfishEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlowfishEncryption.m; sourceTree = ""; }; 4C46A1B920EC705F00094EA4 /* BlowfishEncryptionBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlowfishEncryptionBase.h; sourceTree = ""; }; 4C46A1BA20EC705F00094EA4 /* BlowfishEncryptionKeyExchange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlowfishEncryptionKeyExchange.h; sourceTree = ""; }; 4C46A1BB20EC705F00094EA4 /* TPIBlowfishEncryptionSwizzledClasses.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPIBlowfishEncryptionSwizzledClasses.m; sourceTree = ""; }; 4C46A1BC20EC705F00094EA4 /* NSDataHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSDataHelper.m; sourceTree = ""; }; 4C46A1BD20EC705F00094EA4 /* BlowfishEncryptionKeyExchangeBase.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BlowfishEncryptionKeyExchangeBase.mm; sourceTree = ""; }; 4C46A1C820EC70A200094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C46A1D120EC70B600094EA4 /* LICENSE-GPLv2.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "LICENSE-GPLv2.txt"; sourceTree = SOURCE_ROOT; }; 4C46A1D220EC70B600094EA4 /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = SOURCE_ROOT; }; 4C46A1D320EC70B600094EA4 /* ACKNOWLEDGEMENT.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ACKNOWLEDGEMENT.txt; sourceTree = SOURCE_ROOT; }; 4C46A1DA20EC70C000094EA4 /* Inherited.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Inherited.entitlements; sourceTree = ""; }; 4C46A1DE20EC70C000094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A1DF20EC70C000094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A1E320EC70C000094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A1E620EC70C000094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A1E720EC70C000094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A1EB20EC70C000094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A1EF20EC70C000094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A1F020EC70C000094EA4 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4C46A1F120EC70C000094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A1F320EC70C000094EA4 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4C46A1F620EC70C000094EA4 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4C46A1FA20EC70C000094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A1FB20EC70C000094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A1FF20EC70C000094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A20220EC70C000094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A20320EC70C000094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A20720EC70C000094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CBE7B4A1A91471A008FB230 /* CocoaExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaExtensions.framework; path = "../../../.tmp/SharedBuildProducts-Frameworks/CocoaExtensions.framework"; sourceTree = SOURCE_ROOT; }; 4CF2849F1F8EE9BA00C14E24 /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcrypto.a; path = "../../../Frameworks/Static Libraries/Libraries/libcrypto.a"; sourceTree = SOURCE_ROOT; }; 8D576316048677EA00EA77CD /* BlowfishEncryption.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BlowfishEncryption.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4CF284A01F8EEA0E00C14E24 /* libcrypto.a in Frameworks */, 4CBE7B4C1A91471A008FB230 /* CocoaExtensions.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* BlowfishCommandLine */ = { isa = PBXGroup; children = ( 4C46A1AE20EC705F00094EA4 /* Classes */, 4C46A1C620EC70A200094EA4 /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = BlowfishCommandLine; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( 4C8F2F6C1AAE661B007821CC /* External Frameworks */, 4CF2849E1F8EE99E00C14E24 /* External Libraries */, 4CD8861D16C09BA00000CF8D /* System Frameworks */, ); name = Frameworks; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* BlowfishEncryption.bundle */, ); name = Products; sourceTree = ""; }; 4C46A1AE20EC705F00094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A1B220EC705F00094EA4 /* BlowfishEncryption.h */, 4C46A1B820EC705F00094EA4 /* BlowfishEncryption.m */, 4C46A1B920EC705F00094EA4 /* BlowfishEncryptionBase.h */, 4C46A1B520EC705F00094EA4 /* BlowfishEncryptionBase.m */, 4C46A1BA20EC705F00094EA4 /* BlowfishEncryptionKeyExchange.h */, 4C46A1B720EC705F00094EA4 /* BlowfishEncryptionKeyExchange.mm */, 4C46A1B120EC705F00094EA4 /* BlowfishEncryptionKeyExchangeBase.h */, 4C46A1BD20EC705F00094EA4 /* BlowfishEncryptionKeyExchangeBase.mm */, 4C46A1B320EC705F00094EA4 /* NSDataHelper.h */, 4C46A1BC20EC705F00094EA4 /* NSDataHelper.m */, 4C46A1AF20EC705F00094EA4 /* TPIBlowfishEncryption.h */, 4C46A1B620EC705F00094EA4 /* TPIBlowfishEncryption.m */, 4C46A1B420EC705F00094EA4 /* TPIBlowfishEncryptionSwizzledClasses.h */, 4C46A1BB20EC705F00094EA4 /* TPIBlowfishEncryptionSwizzledClasses.m */, ); path = Classes; sourceTree = ""; }; 4C46A1C620EC70A200094EA4 /* Resources */ = { isa = PBXGroup; children = ( 4C46A1D720EC70C000094EA4 /* Configurations */, 4C46A1D020EC70AB00094EA4 /* Documents */, 4C46A1C920EC70A200094EA4 /* Language Files */, 4C46A1C720EC70A200094EA4 /* Property Lists */, 4C46A1CB20EC70A200094EA4 /* User Interface */, ); path = Resources; sourceTree = ""; }; 4C46A1C720EC70A200094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A1C820EC70A200094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A1C920EC70A200094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C36AAFE20F27F95007CA939 /* BasicLanguage.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C46A1CB20EC70A200094EA4 /* User Interface */ = { isa = PBXGroup; children = ( 4C36AAFB20F27F8E007CA939 /* TPIBlowfishEncryption.xib */, ); path = "User Interface"; sourceTree = ""; }; 4C46A1D020EC70AB00094EA4 /* Documents */ = { isa = PBXGroup; children = ( 4C46A1D320EC70B600094EA4 /* ACKNOWLEDGEMENT.txt */, 4C46A1D120EC70B600094EA4 /* LICENSE-GPLv2.txt */, 4C46A1D220EC70B600094EA4 /* LICENSE.txt */, ); name = Documents; sourceTree = ""; }; 4C46A1D720EC70C000094EA4 /* Configurations */ = { isa = PBXGroup; children = ( 4C46A1DB20EC70C000094EA4 /* Build */, 4C46A1D920EC70C000094EA4 /* Sandbox */, ); name = Configurations; path = ../../../Configurations; sourceTree = SOURCE_ROOT; }; 4C46A1D920EC70C000094EA4 /* Sandbox */ = { isa = PBXGroup; children = ( 4C46A1DA20EC70C000094EA4 /* Inherited.entitlements */, ); path = Sandbox; sourceTree = ""; }; 4C46A1DB20EC70C000094EA4 /* Build */ = { isa = PBXGroup; children = ( 4C46A1E420EC70C000094EA4 /* App Store Release */, 4C46A1ED20EC70C000094EA4 /* Common */, 4C46A20020EC70C000094EA4 /* Debug */, 4C46A1DC20EC70C000094EA4 /* Standard Release */, 4C46A1F820EC70C000094EA4 /* Standard Release Sandboxed */, ); path = Build; sourceTree = ""; }; 4C46A1DC20EC70C000094EA4 /* Standard Release */ = { isa = PBXGroup; children = ( 4C46A1E320EC70C000094EA4 /* Enabled Features.xcconfig */, 4C46A1DF20EC70C000094EA4 /* Textual Extensions.xcconfig */, 4C46A1DE20EC70C000094EA4 /* Textual.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4C46A1E420EC70C000094EA4 /* App Store Release */ = { isa = PBXGroup; children = ( 4C46A1EB20EC70C000094EA4 /* Enabled Features.xcconfig */, 4C46A1E720EC70C000094EA4 /* Textual Extensions.xcconfig */, 4C46A1E620EC70C000094EA4 /* Textual.xcconfig */, ); path = "App Store Release"; sourceTree = ""; }; 4C46A1ED20EC70C000094EA4 /* Common */ = { isa = PBXGroup; children = ( 4C46A1F620EC70C000094EA4 /* Foundation Debug.xcconfig */, 4C46A1F320EC70C000094EA4 /* Foundation.xcconfig */, 4C46A1F020EC70C000094EA4 /* Preserve Symbols.xcconfig */, 4C46A1F120EC70C000094EA4 /* Textual Extensions.xcconfig */, 4C46A1EF20EC70C000094EA4 /* Textual.xcconfig */, ); path = Common; sourceTree = ""; }; 4C46A1F820EC70C000094EA4 /* Standard Release Sandboxed */ = { isa = PBXGroup; children = ( 4C46A1FF20EC70C000094EA4 /* Enabled Features.xcconfig */, 4C46A1FB20EC70C000094EA4 /* Textual Extensions.xcconfig */, 4C46A1FA20EC70C000094EA4 /* Textual.xcconfig */, ); path = "Standard Release Sandboxed"; sourceTree = ""; }; 4C46A20020EC70C000094EA4 /* Debug */ = { isa = PBXGroup; children = ( 4C46A20720EC70C000094EA4 /* Enabled Features.xcconfig */, 4C46A20320EC70C000094EA4 /* Textual Extensions.xcconfig */, 4C46A20220EC70C000094EA4 /* Textual.xcconfig */, ); path = Debug; sourceTree = ""; }; 4C8F2F6C1AAE661B007821CC /* External Frameworks */ = { isa = PBXGroup; children = ( 4CBE7B4A1A91471A008FB230 /* CocoaExtensions.framework */, ); name = "External Frameworks"; sourceTree = ""; }; 4CD8861D16C09BA00000CF8D /* System Frameworks */ = { isa = PBXGroup; children = ( ); name = "System Frameworks"; sourceTree = ""; }; 4CF2849E1F8EE99E00C14E24 /* External Libraries */ = { isa = PBXGroup; children = ( 4CF2849F1F8EE9BA00C14E24 /* libcrypto.a */, ); name = "External Libraries"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* Blowfish Encryption Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Blowfish Encryption Extension" */; buildPhases = ( 4CD4026F16C8CE4E00FEA686 /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "Blowfish Encryption Extension"; productInstallPath = "$(HOME)/Library/Bundles"; productName = BlowfishCommandLine; productReference = 8D576316048677EA00EA77CD /* BlowfishEncryption.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1250; TargetAttributes = { 8D57630D048677EA00EA77CD = { DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Blowfish Encryption Extension" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( en, Base, ); mainGroup = 089C166AFE841209C02AAC07 /* BlowfishCommandLine */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* Blowfish Encryption Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4CD4026F16C8CE4E00FEA686 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C36AAFD20F27F8E007CA939 /* TPIBlowfishEncryption.xib in Resources */, 4C36AB0020F27F95007CA939 /* BasicLanguage.strings in Resources */, 4C46A1D420EC70B600094EA4 /* LICENSE-GPLv2.txt in Resources */, 4C46A1D520EC70B600094EA4 /* LICENSE.txt in Resources */, 4C46A1D620EC70B600094EA4 /* ACKNOWLEDGEMENT.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A1C220EC705F00094EA4 /* BlowfishEncryption.m in Sources */, 4C46A1C420EC705F00094EA4 /* NSDataHelper.m in Sources */, 4C46A1C320EC705F00094EA4 /* TPIBlowfishEncryptionSwizzledClasses.m in Sources */, 4C46A1C520EC705F00094EA4 /* BlowfishEncryptionKeyExchangeBase.mm in Sources */, 4C46A1BF20EC705F00094EA4 /* BlowfishEncryptionBase.m in Sources */, 4C46A1C120EC705F00094EA4 /* BlowfishEncryptionKeyExchange.mm in Sources */, 4C46A1C020EC705F00094EA4 /* TPIBlowfishEncryption.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C36AAFB20F27F8E007CA939 /* TPIBlowfishEncryption.xib */ = { isa = PBXVariantGroup; children = ( 4C36AAFC20F27F8E007CA939 /* en */, ); name = TPIBlowfishEncryption.xib; sourceTree = ""; }; 4C36AAFE20F27F95007CA939 /* BasicLanguage.strings */ = { isa = PBXVariantGroup; children = ( 4C36AAFF20F27F95007CA939 /* en */, ); name = BasicLanguage.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1DEB911B08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C46A20320EC70C000094EA4 /* Textual Extensions.xcconfig */; buildSettings = { HEADER_SEARCH_PATHS = ( "${inherited}", "\"${TEXTUAL_WORKSPACE_DIR}/Frameworks/Static Libraries/Headers/**\"", "\"${TEXTUAL_WORKSPACE_DIR}/Sources/App/Classes/Headers/Private/**\"", ); INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; LIBRARY_SEARCH_PATHS = ( "${inherited}", "\"${TEXTUAL_WORKSPACE_DIR}/Frameworks/Static Libraries/Libraries/**\"", ); PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-BlowfishEncryption"; PRODUCT_NAME = BlowfishEncryption; }; name = Debug; }; 1DEB911C08733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { HEADER_SEARCH_PATHS = ( "${inherited}", "\"${TEXTUAL_WORKSPACE_DIR}/Frameworks/Static Libraries/Headers/**\"", "\"${TEXTUAL_WORKSPACE_DIR}/Sources/App/Classes/Headers/Private/**\"", ); INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; LIBRARY_SEARCH_PATHS = ( "${inherited}", "\"${TEXTUAL_WORKSPACE_DIR}/Frameworks/Static Libraries/Libraries/**\"", ); PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-BlowfishEncryption"; PRODUCT_NAME = BlowfishEncryption; }; name = Release; }; 1DEB911F08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; 1DEB912008733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C46A1DF20EC70C000094EA4 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4CCD21D81FA41CE80056F5FE /* Release (App Store) */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C46A1E720EC70C000094EA4 /* Textual Extensions.xcconfig */; buildSettings = { }; name = "Release (App Store)"; }; 4CCD21D91FA41CE80056F5FE /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { HEADER_SEARCH_PATHS = ( "${inherited}", "\"${TEXTUAL_WORKSPACE_DIR}/Frameworks/Static Libraries/Headers/**\"", "\"${TEXTUAL_WORKSPACE_DIR}/Sources/App/Classes/Headers/Private/**\"", ); INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; LIBRARY_SEARCH_PATHS = ( "${inherited}", "\"${TEXTUAL_WORKSPACE_DIR}/Frameworks/Static Libraries/Libraries/**\"", ); PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-BlowfishEncryption"; PRODUCT_NAME = BlowfishEncryption; }; name = "Release (App Store)"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Blowfish Encryption Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911B08733D790010E9CD /* Debug */, 1DEB911C08733D790010E9CD /* Release */, 4CCD21D91FA41CE80056F5FE /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Blowfish Encryption Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911F08733D790010E9CD /* Debug */, 1DEB912008733D790010E9CD /* Release */, 4CCD21D81FA41CE80056F5FE /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Plugins/Blowfish Encryption/Blowfish Encryption Extension.xcodeproj/xcshareddata/xcschemes/Blowfish Encryption Extension.xcscheme ================================================ ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/BlowfishEncryption.h ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import typedef enum EKBlowfishEncryptionModeOfOperation : NSInteger { EKBlowfishEncryptionNoneModeOfOperation = -1, // Does nothing, just returns original EKBlowfishEncryptionDefaultModeOfOperation = 0, // Default goes to ECB EKBlowfishEncryptionECBModeOfOperation = 1, EKBlowfishEncryptionCBCModeOfOperation = 2 } EKBlowfishEncryptionModeOfOperation; @interface EKBlowfishEncryption : NSObject + (NSUInteger)estimatedLengthOfEncodedDataOfLength:(NSUInteger)dataLength; + (NSString *)encodeData:(NSString *)input key:(NSString *)phrase mode:(EKBlowfishEncryptionModeOfOperation)mode encoding:(NSStringEncoding)local; + (NSString *)decodeData:(NSString *)input key:(NSString *)phrase mode:(EKBlowfishEncryptionModeOfOperation)mode encoding:(NSStringEncoding)local lostBytes:(NSInteger *)lostBytes; @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/BlowfishEncryption.m ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "BlowfishEncryption.h" #import "BlowfishEncryptionBase.h" @implementation EKBlowfishEncryption + (NSUInteger)estimatedLengthOfEncodedDataOfLength:(NSUInteger)dataLength { return [EKBlowfishEncryptionBase estimatedLengthOfEncodedDataOfLength:dataLength]; } + (NSString *)encodeData:(NSString *)input key:(NSString *)phrase mode:(EKBlowfishEncryptionModeOfOperation)mode encoding:(NSStringEncoding)local { if (mode == EKBlowfishEncryptionNoneModeOfOperation) { return input; } if ([phrase length] > 56) { NSLog(@"[EKBlowfishEncryption] WARNING: Using a key length greater than 56 will result in that key itself being truncated to the first 56 characters."); phrase = [phrase substringToIndex:56]; } NSString *result = [EKBlowfishEncryptionBase encrypt:input key:phrase mode:mode encoding:local]; if ([result length] <= 0) { return nil; } if (mode == EKBlowfishEncryptionCBCModeOfOperation) { return [@"+OK *" stringByAppendingString:result]; } else { return [@"+OK " stringByAppendingString:result]; } } + (NSString *)decodeData:(NSString *)input key:(NSString *)phrase mode:(EKBlowfishEncryptionModeOfOperation)mode encoding:(NSStringEncoding)local lostBytes:(NSInteger *)lostBytes { if (mode == EKBlowfishEncryptionNoneModeOfOperation) { return input; } BOOL hasOKPrefix = [input hasPrefix:@"+OK "]; BOOL hasMCPSPrefix = [input hasPrefix:@"mcps "]; if ((hasOKPrefix || hasMCPSPrefix)) { if (hasOKPrefix) { if ([input length] == 4) { return @" "; /* Allow for empty strings. */ } else { input = [input substringFromIndex:4]; } } else if (hasMCPSPrefix) { if ([input length] == 5) { return @" "; /* Allow for empty strings. */ } else { input = [input substringFromIndex:5]; } } } else { return nil; } /* Star symbol acts as an auto-on. */ if ([input hasPrefix:@"*"]) { input = [input substringFromIndex:1]; mode = EKBlowfishEncryptionCBCModeOfOperation; } else { mode = EKBlowfishEncryptionECBModeOfOperation; } if ([phrase length] > 56) { NSLog(@"[EKBlowfishEncryption] WARNING: Using a key length greater than 56 will result in that key itself being truncated to the first 56 characters."); phrase = [phrase substringToIndex:56]; } NSString *result = [EKBlowfishEncryptionBase decrypt:input key:phrase mode:mode encoding:local lostBytes:lostBytes]; if ([result length] <= 0) { return nil; } return result; } @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/BlowfishEncryptionBase.h ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "BlowfishEncryption.h" // for enum @interface EKBlowfishEncryptionBase : NSObject + (NSUInteger)estimatedLengthOfEncodedDataOfLength:(NSUInteger)dataLength; + (NSString *)encrypt:(NSString *)rawInput key:(NSString *)secretKey mode:(EKBlowfishEncryptionModeOfOperation)mode encoding:(NSStringEncoding)dataEncoding; + (NSString *)decrypt:(NSString *)rawInput key:(NSString *)secretKey mode:(EKBlowfishEncryptionModeOfOperation)mode encoding:(NSStringEncoding)dataEncoding lostBytes:(NSInteger *)lostBytes; @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/BlowfishEncryptionBase.m ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* A portion of this source file contains copyrighted work derived from one or more 3rd party, open source projects. The use of this work is hereby acknowledged. */ // Copyright (c) 2005-2013 Mathias Karlsson // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // Please see LICENSE-GPLv2.txt for further information. #import "BlowfishEncryptionBase.h" #import "NSDataHelper.h" #import #import #import static const char blowfish_ecb_base64_chars[64] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; #pragma mark - #pragma mark Implementation. @implementation EKBlowfishEncryptionBase + (NSUInteger)estimatedLengthOfEncodedDataOfLength:(NSUInteger)dataLength { /* The returned estimation is the result of base64 encoded, properly padded encryption block up to input length. */ NSUInteger blockLength = dataLength; NSUInteger blockLengthRemainder = (blockLength % kCCBlockSizeBlowfish); if (blockLengthRemainder != 0) { blockLength += (kCCBlockSizeBlowfish - blockLengthRemainder); } return (ceil(blockLength / 3) * 4); } + (NSString *)encrypt:(NSString *)rawInput key:(NSString *)secretKey mode:(EKBlowfishEncryptionModeOfOperation)mode encoding:(NSStringEncoding)dataEncoding { if (mode == EKBlowfishEncryptionDefaultModeOfOperation || mode == EKBlowfishEncryptionECBModeOfOperation) { return [self ecb_encrypt:rawInput key:secretKey encoding:dataEncoding]; } else { return [self cbc_encrypt:rawInput key:secretKey encoding:dataEncoding]; } } + (NSString *)decrypt:(NSString *)rawInput key:(NSString *)secretKey mode:(EKBlowfishEncryptionModeOfOperation)mode encoding:(NSStringEncoding)dataEncoding lostBytes:(NSInteger *)lostBytes { if (mode == EKBlowfishEncryptionDefaultModeOfOperation || mode == EKBlowfishEncryptionECBModeOfOperation) { return [self ecb_decrypt:rawInput key:secretKey encoding:dataEncoding lostBytes:lostBytes]; } else { return [self cbc_decrypt:rawInput key:secretKey encoding:dataEncoding]; } } NSData *_commonCryptoInitializationVector(void) { uint8_t initializationVector[kCCBlockSizeBlowfish] = {0}; CCRNGStatus cryptoRandomBytesStatus = CCRandomGenerateBytes(&initializationVector, 8); if (cryptoRandomBytesStatus == kCCSuccess) { return [NSData dataWithBytes:initializationVector length:kCCBlockSizeBlowfish]; } else { return nil; } } NSData *_performCommonCryptoOperation(CCOperation ccInOperation, CCAlgorithm ccInOperationAlgorithm, CCMode ccInOperationMode, BOOL ccInOperationUsesPadding, NSData *ccInitializationVector, NSData *ccInSecretKey, NSData *ccInRelatedData) { /* Perform basic validation on input data. */ if (ccInSecretKey == nil || ccInRelatedData == nil) { return nil; } /* Validate key length is within an expected range. */ switch (ccInOperationAlgorithm) { case kCCAlgorithmAES128: { if ([ccInSecretKey length] != kCCKeySizeAES128 && [ccInSecretKey length] != kCCKeySizeAES192 && [ccInSecretKey length] != kCCKeySizeAES256) { return nil; } break; } case kCCAlgorithmDES: { if ([ccInSecretKey length] != kCCKeySizeDES) { return nil; } break; } case kCCAlgorithm3DES: { if ([ccInSecretKey length] != kCCKeySize3DES) { return nil; } break; } case kCCAlgorithmCAST: { if ([ccInSecretKey length] < kCCKeySizeMinCAST || [ccInSecretKey length] > kCCKeySizeMaxCAST) { return nil; } break; } case kCCAlgorithmRC4: { if ([ccInSecretKey length] < kCCKeySizeMinRC4 || [ccInSecretKey length] > kCCKeySizeMaxRC4) { return nil; } break; } case kCCAlgorithmRC2: { if ([ccInSecretKey length] < kCCKeySizeMinRC2 || [ccInSecretKey length] > kCCKeySizeMaxRC2) { return nil; } break; } case kCCAlgorithmBlowfish: { if (/* [ccInSecretKey length] < kCCKeySizeMinBlowfish || */ [ccInSecretKey length] > kCCKeySizeMaxBlowfish) { return nil; } break; } } /* Attempt to create a cryptor reference using input. */ CCPadding ccInPadding; if (ccInOperationUsesPadding) { ccInPadding = ccPKCS7Padding; } else { ccInPadding = ccNoPadding; } CCCryptorRef cryptorRef; CCCryptorStatus cryptorCreateStatus = CCCryptorCreateWithMode(ccInOperation, ccInOperationMode, ccInOperationAlgorithm, ccInPadding, [ccInitializationVector bytes], [ccInSecretKey bytes], [ccInSecretKey length], NULL, 0, 0, 0, &cryptorRef); if (cryptorCreateStatus != kCCSuccess) { return nil; } /* Create output buffer using expected output length. */ BOOL willCallCryptorFinal = (ccInPadding == ccPKCS7Padding || ccInOperationAlgorithm == kCCAlgorithmRC4); size_t outputBufferSize = CCCryptorGetOutputLength(cryptorRef, [ccInRelatedData length], willCallCryptorFinal); NSMutableData *outputBuffer = [NSMutableData dataWithLength:outputBufferSize];; /* Perform update operation on the cryptor. */ size_t cryptorUpdateDataOutMoved = 0; CCCryptorStatus cryptorUpdateStatus = CCCryptorUpdate(cryptorRef, [ccInRelatedData bytes], [ccInRelatedData length], [outputBuffer mutableBytes], [outputBuffer length], &cryptorUpdateDataOutMoved); if (cryptorUpdateStatus != kCCSuccess) { goto cleanup_function; } /* Perform final operation on the cryptor. */ size_t totalBytesWritten = cryptorUpdateDataOutMoved; if (willCallCryptorFinal) { void *cryptorFinalDataOut = ([outputBuffer mutableBytes] + cryptorUpdateDataOutMoved); size_t cryptorFinalDataOutSize = ([outputBuffer length] - cryptorUpdateDataOutMoved); CCCryptorStatus cryptorFinalStatus = CCCryptorFinal(cryptorRef, cryptorFinalDataOut, cryptorFinalDataOutSize, &cryptorUpdateDataOutMoved); if (cryptorFinalStatus != kCCSuccess) { goto cleanup_function; } totalBytesWritten += cryptorUpdateDataOutMoved; [outputBuffer setLength:totalBytesWritten]; } cleanup_function: if (cryptorRef) { CCCryptorRelease(cryptorRef); } return outputBuffer; } #pragma mark - #pragma mark CBC Encryption + (NSString *)cbc_encrypt:(NSString *)rawInput key:(NSString *)secretKey encoding:(NSStringEncoding)dataEncoding { NSData *secretKeyData = [secretKey dataUsingEncoding:dataEncoding]; NSData *rawInputData = [rawInput dataUsingEncoding:dataEncoding paddedByBytes:kCCBlockSizeBlowfish]; NSData *initializationVectorData = _commonCryptoInitializationVector(); if (initializationVectorData == nil) { return nil; } /* mIRC fish 10 places the initialization vector directly on the data stream rather than allowing the crypto library to handle that itself. If we do not append the IV here and instead feed it to Common Crypto, mIRC will not handle the operation properly. */ NSMutableData *objectToEncrypt = [NSMutableData data]; [objectToEncrypt appendData:initializationVectorData]; [objectToEncrypt appendData:rawInputData]; NSData *encryptedData = _performCommonCryptoOperation(kCCEncrypt, kCCAlgorithmBlowfish, kCCModeCBC, NO, nil, secretKeyData, objectToEncrypt); return [XRBase64Encoding encodeData:encryptedData]; } #pragma mark - #pragma mark CBC Decryption + (NSString *)cbc_decrypt:(NSString *)rawInput key:(NSString *)secretKey encoding:(NSStringEncoding)dataEncoding { NSData *secretKeyData = [secretKey dataUsingEncoding:dataEncoding]; NSData *rawInputData = [XRBase64Encoding decodeData:rawInput]; NSData *decryptedData = _performCommonCryptoOperation(kCCDecrypt, kCCAlgorithmBlowfish, kCCModeCBC, NO, nil, secretKeyData, rawInputData); /* If we contain at least two blocks, then remove the first block. mIRC fish 10 has the IV in the first block then we want at least one block of user data. */ if ([decryptedData length] >= (kCCBlockSizeBlowfish * 2)) { decryptedData = [decryptedData subdataWithRange:NSMakeRange(kCCBlockSizeBlowfish, ([decryptedData length] - kCCBlockSizeBlowfish))]; } else { return nil; } NSMutableData *decryptedDataCleaned = [decryptedData mutableCopy]; [decryptedDataCleaned removeBadCharacters]; return [[NSString alloc] initWithData:decryptedDataCleaned encoding:dataEncoding]; } #pragma mark - #pragma mark ECB Mode Encryption + (NSString *)ecb_encrypt:(NSString *)rawInput key:(NSString *)secretKey encoding:(NSStringEncoding)dataEncoding { NSData *secretKeyData = [secretKey dataUsingEncoding:dataEncoding]; NSData *rawInputData = [rawInput dataUsingEncoding:dataEncoding paddedByBytes:kCCBlockSizeBlowfish]; NSData *encryptedData = _performCommonCryptoOperation(kCCEncrypt, kCCAlgorithmBlowfish, kCCModeECB, NO, nil, secretKeyData, rawInputData); NSString *encryptedString = [EKBlowfishEncryptionBase ecb_encrypt_base64Encode:encryptedData]; return encryptedString; } + (NSString *)ecb_decrypt:(NSString *)rawInput key:(NSString *)secretKey encoding:(NSStringEncoding)dataEncoding lostBytes:(NSInteger *)lostBytes { NSData *secretKeyData = [secretKey dataUsingEncoding:dataEncoding]; NSData *encodedRawData = [rawInput dataUsingEncoding:dataEncoding fitToPadding:12 trimmedCharacters:lostBytes]; NSData *rawInputData = [EKBlowfishEncryptionBase ecb_decrypt_base64Decode:encodedRawData]; NSData *decryptedData = _performCommonCryptoOperation(kCCDecrypt, kCCAlgorithmBlowfish, kCCModeECB, NO, nil, secretKeyData, rawInputData); NSMutableData *decryptedDataCleaned = [decryptedData mutableCopy]; [decryptedDataCleaned removeBadCharacters]; return [[NSString alloc] initWithData:decryptedDataCleaned encoding:dataEncoding]; } #pragma mark - #pragma mark ECB Mode Encoding + (NSString *)ecb_encrypt_base64Encode:(NSData *)encryptedData { if (encryptedData == nil) { return nil; } if (([encryptedData length] % kCCBlockSizeBlowfish) != 0) { return nil; } NSMutableData *outputBuffer = [NSMutableData data]; unsigned char *s = (unsigned char *)[encryptedData bytes]; for (NSInteger i = 0; i < [encryptedData length]; i += 8) { unsigned int left; unsigned int right; left = (*s++ << 24); left += (*s++ << 16); left += (*s++ << 8); left += *s++; right = (*s++ << 24); right += (*s++ << 16); right += (*s++ << 8); right += *s++; for (NSInteger k = 0; k < 6; k++) { unsigned char partChar = blowfish_ecb_base64_chars[(right & 0x3f)]; [outputBuffer appendBytes:&partChar length:sizeof(partChar)]; right = (right >> 6); } for (NSInteger k = 0; k < 6; k++) { unsigned char partChar = blowfish_ecb_base64_chars[(left & 0x3f)]; [outputBuffer appendBytes:&partChar length:sizeof(partChar)]; left = (left >> 6); } } return [[NSString alloc] initWithData:outputBuffer encoding:NSASCIIStringEncoding]; } + (int)ecb_decrypt_base64DecodeCharacterIndex:(char)c { int i = (-1); for (i = 0; i < 64; i++) { if (blowfish_ecb_base64_chars[i] == c) { return i; } } return i; } + (NSData *)ecb_decrypt_base64Decode:(NSData *)dataToDecrypt { if (dataToDecrypt == nil) { return nil; } if (([dataToDecrypt length] % 12) != 0) { return nil; } NSMutableData *outputBuffer = [NSMutableData data]; unsigned char *s = (unsigned char *)[dataToDecrypt bytes]; for (NSInteger i = 0; i < [dataToDecrypt length]; i += 12) { unsigned int left = 0; unsigned int right = 0; for (NSInteger k = 0; k < 6; k++) { int partChar = [EKBlowfishEncryptionBase ecb_decrypt_base64DecodeCharacterIndex:(*s++)]; if (partChar == (-1)) { return nil; // Bad character in block } right |= (partChar << k * 6); } for (NSInteger k = 0; k < 6; k++) { int partChar = [EKBlowfishEncryptionBase ecb_decrypt_base64DecodeCharacterIndex:(*s++)]; if (partChar == (-1)) { return nil; // Bad character in block } left |= (partChar << k * 6); } uint8_t bufferByte[8]; bufferByte[0] = ((left >> 24) & 0xFF); bufferByte[1] = ((left >> 16) & 0xFF); bufferByte[2] = ((left >> 8) & 0xFF); bufferByte[3] = (left & 0xFF); bufferByte[4] = ((right >> 24) & 0xFF); bufferByte[5] = ((right >> 16) & 0xFF); bufferByte[6] = ((right >> 8) & 0xFF); bufferByte[7] = (right & 0xFF); [outputBuffer appendBytes:&bufferByte length:sizeof(bufferByte)]; } return [outputBuffer copy]; } @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/BlowfishEncryptionKeyExchange.h ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import @interface EKBlowfishEncryptionKeyExchange : NSObject - (NSString *)generatePublicKey; - (NSString *)secretKeyFromPublicKey:(NSString *)publicKey; @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/BlowfishEncryptionKeyExchange.mm ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "BlowfishEncryptionKeyExchange.h" #import "BlowfishEncryptionKeyExchangeBase.h" @interface EKBlowfishEncryptionKeyExchange () @property (nonatomic, strong) EKBlowfishEncryptionKeyExchangeBase *keyExchanger; @end @implementation EKBlowfishEncryptionKeyExchange #pragma mark - - (instancetype)init { if ((self = [super init])) { self.keyExchanger = [EKBlowfishEncryptionKeyExchangeBase new]; return self; } return nil; } - (void)dealloc { self.keyExchanger = nil; } #pragma mark - - (NSString *)generatePublicKey { NSData *publicKeyRaw = [[self keyExchanger] rawPublicKey]; if ([publicKeyRaw length] >= 1) { return [[self keyExchanger] publicKeyValue:publicKeyRaw]; } return nil; } - (NSString *)secretKeyFromPublicKey:(NSString *)publicKey { NSData *publicKeyData = [[self keyExchanger] base64Decode:publicKey]; if ([publicKeyData length] < EKBlowfishEncryptionKeyExchangeRequiredKeyLength || [publicKeyData length] > EKBlowfishEncryptionKeyExchangeRequiredKeyLength) { return nil; } [[self keyExchanger] setKeyForComputation:publicKeyData]; [[self keyExchanger] computeKey]; NSString *secretString = [[self keyExchanger] secretStringValue]; if ([secretString length] <= 0) { return nil; } return secretString; } @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/BlowfishEncryptionKeyExchangeBase.h ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* A portion of this source file contains copyrighted work derived from one or more 3rd party, open source projects. The use of this work is hereby acknowledged. */ // Copyright (c) 2005-2013 Mathias Karlsson // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // Please see LICENSE-GPLv2.txt for further information. #import #define EKBlowfishEncryptionKeyExchangeRequiredKeyLength 135 @interface EKBlowfishEncryptionKeyExchangeBase : NSObject - (void)computeKey; - (void)setKeyForComputation:(NSData *)publicKey; - (void)resetStatus; - (void)resetPublicInformation; - (NSString *)secretStringValue; - (NSData *)rawPublicKey; - (NSString *)publicKeyValue:(NSData *)publicInput; - (NSString *)base64Encode:(NSData *)input; - (NSData *)base64Decode:(NSString *)input; @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/BlowfishEncryptionKeyExchangeBase.mm ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* A portion of this source file contains copyrighted work derived from one or more 3rd party, open source projects. The use of this work is hereby acknowledged. */ // Copyright (c) 2005-2013 Mathias Karlsson // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // Please see LICENSE-GPLv2.txt for further information. #import "BlowfishEncryptionKeyExchangeBase.h" #import /* OpenSSL Header Files. */ #include #include #include /* Private Interface. */ @interface EKBlowfishEncryptionKeyExchangeBase () @property (nonatomic, strong) NSData *secretValue; @property (nonatomic, unsafe_unretained) DH *DHStatus; @property (nonatomic, unsafe_unretained) BIGNUM *publicBigNum; @end /* Static Values. */ /* fishPrimeB64 is the exact value of the prime used by the original DH1080 implementation. DH->p and DH->g need to be the same value for each user involved in a Diffie-Hellman key exchange. Therefore, to ensure compatibility with existing users, we have used the same prime value (DH->p) as well as using "2" for DH->g. DH1080Base also matches the Base64 format for encoding and decoding. */ static NSString *fishPrimeB64 = @"++ECLiPSE+is+proud+to+present+latest+FiSH+release+featuring+even+more+security+for+you+++shouts+go+out+to+TMG+for+helping+to+generate+this+cool+sophie+germain+prime+number++++/C32L"; /* Static Definitions. */ #define DHAssertNO(c) if (c == NO) { NSAssert(NO, @"DH1080 Key Exchange Failure."); } #define DHAssertYES(c) if (c) { NSAssert(NO, @"DH1080 Key Exchange Failure."); } /* Implementation. */ @implementation EKBlowfishEncryptionKeyExchangeBase #pragma mark - - (id)init { if ((self = [super init])) { self.DHStatus = 0; self.publicBigNum = 0; [self initializeKeyExchange]; return self; } return nil; } - (void)dealloc { [self resetStatus]; [self resetPublicInformation]; } - (void)resetPublicInformation { if (self.publicBigNum != 0) { BN_free(self.publicBigNum); self.publicBigNum = 0; } } - (void)resetStatus { if (self.DHStatus != 0) { DH_free(self.DHStatus); self.DHStatus = 0; } } #pragma mark - - (void)initializeKeyExchange { DHAssertNO(self.DHStatus == 0); self.DHStatus = DH_new(); DHAssertYES(self.DHStatus == 0); DHAssertNO(DH_get0_g(self.DHStatus) == 0); DHAssertNO(DH_get0_p(self.DHStatus) == 0); NSData *primeData = [self base64Decode:fishPrimeB64]; BIGNUM *g = BN_new(); BIGNUM *p = BN_new(); DH_set0_pqg(self.DHStatus, p, NULL, g); BN_dec2bn(&g, "2"); DHAssertNO([primeData length] >= 1); BIGNUM *ret = BN_bin2bn((unsigned char *)[primeData bytes], (int)[primeData length], p); DHAssertYES(ret == 0); DHAssertYES(DH_get0_g(self.DHStatus) == 0); DHAssertYES(DH_get0_p(self.DHStatus) == 0); int check, codes; check = DH_check(self.DHStatus, &codes); DHAssertNO(check == 1); DHAssertNO(codes == 0); int genr = DH_generate_key(self.DHStatus); DHAssertNO(genr == 1); } #pragma mark - - (void)computeKey { DHAssertYES(self.DHStatus == 0); DHAssertYES(DH_get0_g(self.DHStatus) == 0); DHAssertYES(DH_get0_p(self.DHStatus) == 0); DHAssertYES(self.publicBigNum == 0); NSInteger size = DH_size(self.DHStatus); DHAssertNO(size == EKBlowfishEncryptionKeyExchangeRequiredKeyLength); unsigned char key[EKBlowfishEncryptionKeyExchangeRequiredKeyLength]; NSInteger num = DH_compute_key(key, self.publicBigNum, self.DHStatus); DHAssertNO(num == size); NSData *secretValue = [[NSData alloc] initWithBytes:key length:sizeof(key)]; DHAssertNO([secretValue length] >= 1); self.secretValue = secretValue; } - (void)setKeyForComputation:(NSData *)publicKey { if (self.publicBigNum == 0) { self.publicBigNum = BN_new(); } DHAssertYES(self.publicBigNum == 0); BIGNUM *ret = BN_bin2bn((unsigned char *)[publicKey bytes], (int)[publicKey length], self.publicBigNum); DHAssertYES(ret == 0); DHAssertYES(self.publicBigNum == 0); } #pragma mark - - (NSString *)secretStringValue { NSData *secretValue = [self secretValue]; DHAssertNO(secretValue.length >= 1); unsigned char sha_md[32]; SHA256((unsigned char *)[secretValue bytes], (int)[secretValue length], sha_md); NSData *secretHash = [[NSData alloc] initWithBytes:sha_md length:sizeof(sha_md)]; DHAssertNO([secretHash length] >= 1); return [self base64Encode:secretHash]; } - (NSString *)publicKeyValue:(NSData *)publicInput { DHAssertNO([publicInput length] >= 1); return [self base64Encode:publicInput]; } - (NSData *)rawPublicKey { DHAssertYES(self.DHStatus == 0); DHAssertYES(DH_get0_g(self.DHStatus) == 0); DHAssertYES(DH_get0_p(self.DHStatus) == 0); NSInteger size = DH_size(self.DHStatus); DHAssertNO(size == EKBlowfishEncryptionKeyExchangeRequiredKeyLength); unsigned char key[EKBlowfishEncryptionKeyExchangeRequiredKeyLength]; const BIGNUM *publicKey = DH_get0_pub_key(self.DHStatus); BN_bn2bin(publicKey, key); NSData *publicInput = [[NSData alloc] initWithBytes:key length:sizeof(key)]; DHAssertNO([publicInput length] >= 1); return publicInput; } #pragma mark - - (NSString *)base64Encode:(NSData *)input { NSString *output = [XRBase64Encoding encodeData:input]; DHAssertNO([output length] >= 1); BOOL equalFound = NO; while (YES) { NSRange equalRange = [output rangeOfString:@"="]; if (equalRange.location == NSNotFound) { if (equalFound == NO) { output = [output stringByAppendingString:@"A"]; } break; } else { equalFound = YES; output = [output substringWithRange:NSMakeRange(0, ([output length] - 1))]; } } return output; } - (NSData *)base64Decode:(NSString *)input { NSInteger inputLength = [input length]; DHAssertNO([input length] >= 1); NSString *ecv = [input substringFromIndex:(inputLength - 1)]; if ((inputLength % 4) == 1 && [ecv isEqualToString:@"A"]) { input = [input substringToIndex:(inputLength - 1)]; } while ((([input length] % 4) == 0) == NO) { input = [input stringByAppendingString:@"="]; } return [XRBase64Encoding decodeData:input]; } @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/NSDataHelper.h ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import @interface NSMutableData (BlowfishEncryptionDatHelper) - (void)removeBadCharacters; @end @interface NSString (BlowfishEncryptionStringHelper) /* If the data object does not fit in the block size, then NULL characters are set to fill in the half-sized block */ - (NSData *)dataUsingEncoding:(NSStringEncoding)encoding paddedByBytes:(NSInteger)bytePadding; /* If the data object does not fit in the block size, then the tail is truncated and the number of bytes lost is returned. */ - (NSData *)dataUsingEncoding:(NSStringEncoding)encoding fitToPadding:(NSInteger)bytePadding trimmedCharacters:(NSInteger *)bytesRemoved; @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/NSDataHelper.m ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSDataHelper.h" @implementation NSMutableData (BlowfishEncryptionDataHelper) - (void)removeBadCharacters { [self replaceAllOccurrencesOfData:[NSData dataWithBytes:"\x0D" length:1] withBytes:NULL length:0]; // Line break [self replaceAllOccurrencesOfData:[NSData dataWithBytes:"\x0A" length:1] withBytes:NULL length:0]; // Line feed [self replaceAllOccurrencesOfData:[NSData dataWithBytes:"\x00" length:1] withBytes:NULL length:0]; // NULL character } - (void)replaceAllOccurrencesOfData:(NSData *)needle withBytes:(const void *)replacementBytes length:(NSUInteger)replacementLength { NSUInteger start = 0; while (1 == 1) { if (start >= [self length]) { break; } NSRange r = [self rangeOfData:needle options:0 range:NSMakeRange(start, ([self length] - start))]; if (r.location == NSNotFound) { break; } [self replaceBytesInRange:r withBytes:replacementBytes length:replacementLength];; start = (r.location + replacementLength + 1); } } @end @implementation NSString (BlowfishEncryptionStringHelper) - (NSData *)dataUsingEncoding:(NSStringEncoding)encoding fitToPadding:(NSInteger)bytePadding trimmedCharacters:(NSInteger *)bytesRemoved { if (bytePadding <= 0) { return nil; } NSData *dataObject = [self dataUsingEncoding:encoding allowLossyConversion:NO]; NSUInteger dataObjectLength = [dataObject length]; NSUInteger dataObjectLengthRemainder = (dataObjectLength % bytePadding); if (dataObjectLengthRemainder > 0) { dataObjectLength -= dataObjectLengthRemainder; if ( bytesRemoved) { *bytesRemoved = dataObjectLengthRemainder; } return [NSData dataWithBytes:[dataObject bytes] length:dataObjectLength]; } else { if ( bytesRemoved) { *bytesRemoved = 0; } return dataObject; } } - (NSData *)dataUsingEncoding:(NSStringEncoding)encoding paddedByBytes:(NSInteger)bytePadding { if (bytePadding <= 0) { return nil; } NSData *dataObject = [self dataUsingEncoding:encoding allowLossyConversion:NO]; NSUInteger dataObjectLength = [dataObject length]; NSUInteger dataObjectLengthRemainder = (dataObjectLength % bytePadding); if (dataObjectLengthRemainder > 0) { NSMutableData *dataObjectMutable = [dataObject mutableCopy]; [dataObjectMutable increaseLengthBy:(bytePadding - dataObjectLengthRemainder)]; return [dataObjectMutable copy]; } else { return dataObject; } } @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/TPIBlowfishEncryption.h ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" #import "BlowfishEncryption.h" #import "BlowfishEncryptionKeyExchange.h" @interface TPIBlowfishEncryption : NSObject + (BOOL)isPluginEnabled; + (NSString *)encryptionKeyForChannel:(IRCChannel *)channel; + (void)setEncryptionKey:(NSString *)encryptionKey forChannel:(IRCChannel *)channel; + (EKBlowfishEncryptionModeOfOperation)encryptionModeOfOperationForChannel:(IRCChannel *)channel; + (void)setEncryptionModeOfOperation:(EKBlowfishEncryptionModeOfOperation)modeOfOperation forChannel:(IRCChannel *)channel; @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/TPIBlowfishEncryption.m ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPIBlowfishEncryption.h" #import "TVCLogControllerPrivate.h" #define TXExchangeRequestPrefix @"DH1080_INIT " #define TXExchangeResponsePrefix @"DH1080_FINISH " #define TXExchangeReuqestTimeoutDelay 10 @interface TPIBlowfishEncryption () @property (nonatomic, strong) IBOutlet NSView *preferencePaneView; /* key format: STRING(" –> ") value format: ARRAY("", "") -keyExchangeDictionaryKey: can be used to generate key. */ @property (nonatomic, strong) NSMutableDictionary *keyExchangeRequests; - (IBAction)preferencesChanged:(id)sender; @end @implementation TPIBlowfishEncryption #pragma mark - #pragma mark Plugin Structure + (BOOL)isPluginEnabled { static BOOL _servicesEnabledChecked = NO; static BOOL _servicesEnabled = NO; if (_servicesEnabledChecked == NO) { _servicesEnabledChecked = YES; _servicesEnabled = [RZUserDefaults() boolForKey:@"Blowfish Encryption Extension -> Enable Service"]; } return _servicesEnabled; } - (BOOL)isPluginEnabled { return [TPIBlowfishEncryption isPluginEnabled]; } - (void)pluginLoadedIntoMemory { [self performBlockOnMainThread:^{ [TPIBundleFromClass() loadNibNamed:@"TPIBlowfishEncryption" owner:self topLevelObjects:nil]; }]; if ([self isPluginEnabled]) { self.keyExchangeRequests = [NSMutableDictionary dictionary]; } } - (void)pluginWillBeUnloadedFromMemory { if ([self isPluginEnabled]) { self.keyExchangeRequests = nil; [NSObject cancelPreviousPerformRequestsWithTarget:self]; } } - (NSString *)pluginPreferencesPaneMenuItemName { return TPILocalizedString(@"BasicLanguage[8o8-tj]"); } - (NSView *)pluginPreferencesPaneView { return self.preferencePaneView; } - (void)preferencesChanged:(id)sender { [TDCAlert alertSheetWithWindow:[NSApp keyWindow] body:TPILocalizedString(@"BasicLanguage[ei0-4a]") title:TPILocalizedString(@"BasicLanguage[0h8-o3]") defaultButton:TPILocalizedString(@"BasicLanguage[4qo-gl]") alternateButton:nil otherButton:nil]; } - (void)didReceiveServerInput:(THOPluginDidReceiveServerInputConcreteObject *)inputObject onClient:(IRCClient *)client { if ([self isPluginEnabled] == NO) { return; // Cancel operation... } [self performBlockOnMainThread:^{ NSString *person = [inputObject senderNickname]; NSString *message = [inputObject messageSequence]; if ([message hasPrefix:@"+"]) { message = [message substringFromIndex:1]; } BOOL isRequest = [message hasPrefix:TXExchangeRequestPrefix]; BOOL isResponse = [message hasPrefix:TXExchangeResponsePrefix]; if (isRequest || isResponse) { if (isRequest) { /* A request may create a query so it must be invoked on the main thread. Creating a channel requires access to WebKit and WebKit will throw an exception because they hate running on anything else. */ [self keyExchangeRequestReceived:message on:client from:person]; } else { /* We do not want to create the channel if it is a response. If the user closed the query, then allow old request to expire. This is done so that the IRCChannel pointer part of our request dictionary will remain same instead of creating a new one and the old pointing to nothing. */ IRCChannel *channel = [client findChannel:person]; if (channel) { NSString *requestKey = [self keyExchangeDictionaryKey:channel]; if (requestKey) { [self keyExchangeResponseReceived:message on:client from:requestKey]; } } } } }]; } - (void)userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString { [self performBlockOnMainThread:^{ if ([self isPluginEnabled] == NO) { [TDCAlert alertWithMessage:TPILocalizedString(@"BasicLanguage[87t-ge]", [commandString lowercaseString]) title:TPILocalizedString(@"BasicLanguage[qb6-7a]") defaultButton:TPILocalizedString(@"BasicLanguage[oyj-zy]") alternateButton:nil suppressionKey:nil suppressionText:nil]; return; // Cancel operation... } IRCChannel *c = [mainWindow() selectedChannelOn:client]; if ([c isChannel] || [c isPrivateMessage]) { NSString *_messageString = [messageString trimAndGetFirstToken]; NSString *encryptionKey = [TPIBlowfishEncryption encryptionKeyForChannel:c]; if ([commandString isEqualToString:@"SETKEY"]) { if (NSObjectIsEmpty(_messageString)) { [TPIBlowfishEncryption setEncryptionKey:nil forChannel:c]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[ime-kt]") inChannel:c]; } else { if (NSObjectIsNotEmpty(encryptionKey)) { if ([encryptionKey isEqualToString:_messageString] == NO) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[9k1-jc]") inChannel:c]; } } else { if ([c isPrivateMessage]) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[rhu-sw]") inChannel:c]; } else { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[1dv-qi]") inChannel:c]; } } if ([_messageString length] > 56) { _messageString = [_messageString substringToIndex:56]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[fmt-65]") inChannel:c]; } [TPIBlowfishEncryption setEncryptionKey:_messageString forChannel:c]; } } else if ([commandString isEqualToString:@"DELKEY"]) { [TPIBlowfishEncryption setEncryptionKey:nil forChannel:c]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[ime-kt]") inChannel:c]; } else if ([commandString isEqualToString:@"KEY"]) { if (NSObjectIsNotEmpty(encryptionKey)) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[st5-x8]", encryptionKey) inChannel:c]; } else { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[sd4-2b]") inChannel:c]; } } else if ([commandString isEqualToString:@"SETKEYMODE"]) { if ([_messageString isEqualIgnoringCase:@"CBC"]) { [TPIBlowfishEncryption setEncryptionModeOfOperation:EKBlowfishEncryptionCBCModeOfOperation forChannel:c]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[vph-s9]") inChannel:c]; } else { [TPIBlowfishEncryption setEncryptionModeOfOperation:EKBlowfishEncryptionECBModeOfOperation forChannel:c]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[5e8-56]") inChannel:c]; } } else if ([commandString isEqualToString:@"KEYX"]) { if ([c isPrivateMessage] == NO) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[2kj-i5]") inChannel:c]; } else { if ([self keyExchangeRequestExists:c]) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[wi6-cy]", [c name]) inChannel:c]; } else if (NSObjectIsNotEmpty(encryptionKey)) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[2oq-wt]", [c name]) inChannel:c]; } else { EKBlowfishEncryptionKeyExchange *keyRequest = [EKBlowfishEncryptionKeyExchange new]; NSString *publicKey = [keyRequest generatePublicKey]; if (NSObjectIsEmpty(publicKey)) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[wo2-d2]") inChannel:c]; } else { NSString *requestKey = [self keyExchangeDictionaryKey:c]; NSString *requestMsg = nil; if ([_messageString isEqualIgnoringCase:@"nocbc"]) { requestMsg = [NSString stringWithFormat:@"%@%@", TXExchangeRequestPrefix, publicKey]; } else { requestMsg = [NSString stringWithFormat:@"%@%@ CBC", TXExchangeRequestPrefix, publicKey]; } [[self keyExchangeRequests] setObject:@[keyRequest, c] forKey:requestKey]; [client send:@"NOTICE", [c name], requestMsg, nil]; [self keyExchangeSetupTimeoutTimer:requestKey]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[ezp-nq]", [c name]) inChannel:c]; } } } } encryptionKey = nil; } }]; } - (NSArray *)subscribedUserInputCommands { return @[@"setkey", @"delkey", @"key", @"keyx", @"setkeymode"]; } - (NSArray *)subscribedServerInputCommands { return @[@"notice"]; } - (NSArray *)pluginOutputSuppressionRules { /* Rule 1 */ THOPluginOutputSuppressionRule *noticeRule1 = [THOPluginOutputSuppressionRule new]; [noticeRule1 setRestrictConsole:YES]; [noticeRule1 setRestrictChannel:YES]; [noticeRule1 setRestrictPrivateMessage:YES]; [noticeRule1 setMatch:[@"^" stringByAppendingString:TXExchangeRequestPrefix]]; /* Rule 2 */ THOPluginOutputSuppressionRule *noticeRule2 = [THOPluginOutputSuppressionRule new]; [noticeRule2 setRestrictConsole:YES]; [noticeRule2 setRestrictChannel:YES]; [noticeRule2 setRestrictPrivateMessage:YES]; [noticeRule2 setMatch:[@"^" stringByAppendingString:TXExchangeResponsePrefix]]; return @[noticeRule1, noticeRule2]; } #pragma mark - #pragma mark Options + (void)setEncryptionKey:(NSString *)encryptionKey forChannel:(IRCChannel *)channel { if (channel) { NSString *serviceName = [NSString stringWithFormat:@"textual.cblowfish.%@", [channel uniqueIdentifier]]; if (encryptionKey == nil) { [XRKeychain deleteKeychainItem:@"Textual (Blowfish Encryption)" withItemKind:@"application password" forUsername:nil serviceName:serviceName]; [[channel viewController] setEncrypted:NO]; } else { [XRKeychain modifyOrAddKeychainItem:@"Textual (Blowfish Encryption)" withItemKind:@"application password" forUsername:nil withNewPassword:encryptionKey serviceName:serviceName]; [[channel viewController] setEncrypted:YES]; } } } + (NSString *)encryptionKeyForChannel:(IRCChannel *)channel { if (channel) { NSString *serviceName = [NSString stringWithFormat:@"textual.cblowfish.%@", [channel uniqueIdentifier]]; return [XRKeychain getPasswordFromKeychainItem:@"Textual (Blowfish Encryption)" withItemKind:@"application password" forUsername:nil serviceName:serviceName]; } else { return nil; } } + (void)setEncryptionModeOfOperation:(EKBlowfishEncryptionModeOfOperation)modeOfOperation forChannel:(IRCChannel *)channel { if (channel) { NSString *defaultsKey = [NSString stringWithFormat:@"Private Extension Store -> Blowfish Encryption Extension -> Encryption Mode of Operation -> %@", [channel uniqueIdentifier]]; if (modeOfOperation == EKBlowfishEncryptionDefaultModeOfOperation) { [RZUserDefaults() removeObjectForKey:defaultsKey]; } else { [RZUserDefaults() setInteger:modeOfOperation forKey:defaultsKey]; } } } + (EKBlowfishEncryptionModeOfOperation)encryptionModeOfOperationForChannel:(IRCChannel *)channel { if (channel) { NSString *defaultsKey = [NSString stringWithFormat:@"Private Extension Store -> Blowfish Encryption Extension -> Encryption Mode of Operation -> %@", [channel uniqueIdentifier]]; id defaultsValue = [RZUserDefaults() objectForKey:defaultsKey]; if (defaultsValue) { return (EKBlowfishEncryptionModeOfOperation)[defaultsValue integerValue]; } else { return EKBlowfishEncryptionDefaultModeOfOperation; } } else { return EKBlowfishEncryptionNoneModeOfOperation; } } #pragma mark - #pragma mark Key Exchange - (void)keyExchangeRequestReceived:(NSString *)requestDataRaw on:(IRCClient *)client from:(NSString *)requestSender { IRCChannel *channel = [client findChannelOrCreate:requestSender isPrivateMessage:YES]; NSString *encryptionKey = [TPIBlowfishEncryption encryptionKeyForChannel:channel]; if (NSObjectIsNotEmpty(encryptionKey)) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[9eq-sn]", [channel name]) inChannel:channel]; return; } EKBlowfishEncryptionModeOfOperation mode = EKBlowfishEncryptionDefaultModeOfOperation; NSString *requestData = nil; if ([requestDataRaw length] > [TXExchangeRequestPrefix length]) { requestData = [requestDataRaw substringFromIndex:[TXExchangeRequestPrefix length]]; NSArray *parts = [requestData split:NSStringWhitespacePlaceholder]; requestData = parts[0]; if ([parts count] > 1 && [parts[1] isEqualToString:@"CBC"]) { mode = EKBlowfishEncryptionCBCModeOfOperation; } } else { requestData = requestDataRaw; } //LogToConsoleDebug("Key Exchange Request Received:"); //LogToConsoleDebug(" Client: %@", client); //LogToConsoleDebug(" Channel: %@", channel); //LogToConsoleDebug(" Message: %@", requestData); if ([requestData length] <= 0) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[lo0-tu]") inChannel:channel]; } else { if ([self keyExchangeRequestExists:channel]) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[8nu-ow]", [channel name]) inChannel:channel]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[wi6-cy]", [channel name]) inChannel:channel]; } else { EKBlowfishEncryptionKeyExchange *keyRequest = [EKBlowfishEncryptionKeyExchange new]; /* Process secret from the Receiver. */ NSString *theSecret = [keyRequest secretKeyFromPublicKey:requestData]; if (NSObjectIsEmpty(theSecret)) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[tr4-jd]") inChannel:channel]; return; } /* Generate our own public key. If everything has gone correctly up to here, then when the user that sent the request computes our public key, we both should have the same secret. */ NSString *publicKey = [keyRequest generatePublicKey]; if (NSObjectIsEmpty(publicKey)) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[wo2-d2]") inChannel:channel]; return; } //LogToConsoleDebug(" Shared Secret: %@", theSecret); [TPIBlowfishEncryption setEncryptionKey:theSecret forChannel:channel]; [TPIBlowfishEncryption setEncryptionModeOfOperation:mode forChannel:channel]; /* Finish up. */ NSString *requestMsg = [TXExchangeResponsePrefix stringByAppendingString:publicKey]; if (mode == EKBlowfishEncryptionCBCModeOfOperation) { requestMsg = [requestMsg stringByAppendingString:@" CBC"]; } [client send:@"NOTICE", [channel name], requestMsg, nil]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[8nu-ow]", [channel name]) inChannel:channel]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[184-19]", [channel name]) inChannel:channel]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[jmi-xp]", [channel name]) inChannel:channel]; if (mode == EKBlowfishEncryptionDefaultModeOfOperation || mode == EKBlowfishEncryptionECBModeOfOperation) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[5p8-18]") inChannel:channel]; } else { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[3px-4w]") inChannel:channel]; } [client printDebugInformation:TPILocalizedString(@"BasicLanguage[cdb-6z]", [channel name]) inChannel:channel]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[7sz-gy]", [channel name]) inChannel:channel]; } } } - (void)keyExchangeResponseReceived:(NSString *)responseDataRaw on:(IRCClient *)client from:(NSString *)responseKey { NSArray *exchangeData = [self keyExchangeInformation:responseKey]; if (exchangeData) { NSString *responseData = nil; EKBlowfishEncryptionModeOfOperation mode = EKBlowfishEncryptionDefaultModeOfOperation; if ([responseDataRaw length] > [TXExchangeResponsePrefix length]) { responseData = [responseDataRaw substringFromIndex:[TXExchangeResponsePrefix length]]; NSArray *parts = [responseData split:NSStringWhitespacePlaceholder]; responseData = parts[0]; if ([parts count] > 1 && [parts[1] isEqualToString:@"CBC"]) { mode = EKBlowfishEncryptionCBCModeOfOperation; } } else { responseData = responseDataRaw; } //LogToConsoleDebug("Key Exchange Response Received:"); //LogToConsoleDebug(" Response Key: %@", responseKey); //LogToConsoleDebug(" Response Info: %@", exchangeData); //LogToConsoleDebug(" Message: %@", responseData); EKBlowfishEncryptionKeyExchange *request = exchangeData[0]; IRCChannel *channel = exchangeData[1]; if ([responseData length] <= 0) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[lo0-tu]") inChannel:channel]; } else { NSString *encryptionKey = [TPIBlowfishEncryption encryptionKeyForChannel:channel]; if (NSObjectIsNotEmpty(encryptionKey)) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[9eq-sn]", [channel name]) inChannel:channel]; return; } [client printDebugInformation:TPILocalizedString(@"BasicLanguage[eqo-bn]", [channel name]) inChannel:channel]; /* Compute the public key received against our own. Our original public key was sent to the user which has responded by computing their own against that. Now we compute the key received to obtain the shared secret. What?… */ NSString *theSecret = [request secretKeyFromPublicKey:responseData]; if (NSObjectIsEmpty(theSecret)) { return [client printDebugInformation:TPILocalizedString(@"BasicLanguage[tr4-jd]") inChannel:channel]; } //LogToConsoleDebug(" Shared Secret: %@", theSecret); [TPIBlowfishEncryption setEncryptionKey:theSecret forChannel:channel]; [TPIBlowfishEncryption setEncryptionModeOfOperation:mode forChannel:channel]; /* Finish up. */ [client printDebugInformation:TPILocalizedString(@"BasicLanguage[jmi-xp]", [channel name]) inChannel:channel]; if (mode == EKBlowfishEncryptionDefaultModeOfOperation || mode == EKBlowfishEncryptionECBModeOfOperation) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[5p8-18]") inChannel:channel]; } else { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[3px-4w]") inChannel:channel]; } [client printDebugInformation:TPILocalizedString(@"BasicLanguage[cdb-6z]", [channel name]) inChannel:channel]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[7sz-gy]", [channel name]) inChannel:channel]; } [[self keyExchangeRequests] removeObjectForKey:responseKey]; } } #pragma mark - #pragma mark Key Exchange Timer. - (void)keyExchangeSetupTimeoutTimer:(NSString *)requestKey { [self performSelector:@selector(keyExchangeTimedOut:) withObject:requestKey afterDelay:TXExchangeReuqestTimeoutDelay]; } - (void)keyExchangeTimedOut:(NSString *)requestKey { NSArray *requestData = [self keyExchangeInformation:requestKey]; if (NSObjectIsNotEmpty(requestKey)) { IRCChannel *channel = requestData[1]; [[channel associatedClient] printDebugInformation:TPILocalizedString(@"BasicLanguage[c3y-2b]", [channel name]) inChannel:channel]; [[self keyExchangeRequests] removeObjectForKey:requestKey]; } } #pragma mark - #pragma mark Key Exchange Information. - (BOOL)keyExchangeRequestExists:(IRCChannel *)channel { NSString *requestKey = [self keyExchangeDictionaryKey:channel]; return NSObjectIsNotEmpty([self keyExchangeInformation:requestKey]); } - (NSArray *)keyExchangeInformation:(NSString *)requestKey { NSArray *requestData = [[self keyExchangeRequests] arrayForKey:requestKey]; if (NSObjectIsNotEmpty(requestData)) { id request = requestData[0]; id channel = requestData[1]; if ([requestData count] == 2 && // Array count is equal to 2. request != nil && // Pointer are not empty. channel != nil && // Pointer are not empty. [request isKindOfClass:[EKBlowfishEncryptionKeyExchange class]] && // Type of class is correct. [channel isKindOfClass:[IRCChannel class]]) { // Type of class is correct. return requestData; } } return nil; } - (NSString *)keyExchangeDictionaryKey:(IRCChannel *)channel { if (channel == nil || [channel isPrivateMessage] == NO) { return nil; } return [NSString stringWithFormat:@"%@ –> %@", [channel uniqueIdentifier], [channel name]]; } @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/TPIBlowfishEncryptionSwizzledClasses.h ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" @interface TPCPreferencesUserDefaults (TPCPreferencesUserDefaultsSwizzled) @end @interface IRCClient (IRCClientSwizzled) @end @interface IRCChannel (IRCChannelSwizzled) @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/Classes/TPIBlowfishEncryptionSwizzledClasses.m ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPIBlowfishEncryption.h" #import "TPIBlowfishEncryptionSwizzledClasses.h" @implementation IRCClient (IRCClientSwizzled) + (void)load { XRExchangeInstanceMethod(@"IRCClient", @"encryptionAllowedForTarget:", @"__tpi_encryptionAllowedForTarget:"); XRExchangeInstanceMethod(@"IRCClient", @"decryptMessage:from:target:decodingCallback:", @"__tpi_decryptMessage:from:target:decodingCallback:"); XRExchangeInstanceMethod(@"IRCClient", @"encryptMessage:directedAt:encodingCallback:injectionCallback:", @"__tpi_encryptMessage:directedAt:encodingCallback:injectionCallback:"); XRExchangeInstanceMethod(@"IRCClient", @"lengthOfEncryptedMessageDirectedAt:thatFitsWithinBounds:", @"__tpi_lengthOfEncryptedMessageDirectedAt:thatFitsWithinBounds:"); } - (BOOL)__tpi_encryptionAllowedForTarget:(NSString *)target { if ([TPIBlowfishEncryption isPluginEnabled] == NO) { return [self __tpi_encryptionAllowedForTarget:target]; } return NO; } - (NSInteger)__tpi_lengthOfEncryptedMessageDirectedAt:(NSString *)messageTo thatFitsWithinBounds:(NSInteger)maximumLength { if ([TPIBlowfishEncryption isPluginEnabled] == NO) { return [self __tpi_lengthOfEncryptedMessageDirectedAt:messageTo thatFitsWithinBounds:maximumLength]; } IRCChannel *targetChannel = [self findChannel:messageTo]; if (targetChannel == nil) { return 0; } NSString *encryptionKey = [TPIBlowfishEncryption encryptionKeyForChannel:targetChannel]; if (encryptionKey == nil) { return 0; } NSInteger lastEstimatedSize = 0; for (NSInteger i = maximumLength; i >= 0; i--) { NSInteger sizeForLength = [EKBlowfishEncryption estimatedLengthOfEncodedDataOfLength:i]; if (sizeForLength < maximumLength) { break; } else { lastEstimatedSize = i; } } return lastEstimatedSize; } - (void)__tpi_encryptMessage:(NSString *)messageBody directedAt:(NSString *)messageTo encodingCallback:(TLOEncryptionManagerEncodingDecodingCallbackBlock)encodingCallback injectionCallback:(TLOEncryptionManagerInjectCallbackBlock)injectionCallback { #define _callback(_encodedString_, _wasEncrypted_) \ if (encodingCallback) { \ encodingCallback(messageBody, _wasEncrypted_); \ } \ if (injectionCallback) { \ injectionCallback(_encodedString_); \ } \ if ([TPIBlowfishEncryption isPluginEnabled] == NO) { [self __tpi_encryptMessage:messageBody directedAt:messageTo encodingCallback:encodingCallback injectionCallback:injectionCallback]; return; } IRCChannel *targetChannel = [self findChannel:messageTo]; if (targetChannel == nil) { _callback(messageBody, NO); return; } NSString *encryptionKey = [TPIBlowfishEncryption encryptionKeyForChannel:targetChannel]; if (encryptionKey == nil) { _callback(messageBody, NO); return; } EKBlowfishEncryptionModeOfOperation decodeMode = [TPIBlowfishEncryption encryptionModeOfOperationForChannel:targetChannel]; NSString *encodedString = [EKBlowfishEncryption encodeData:messageBody key:encryptionKey mode:decodeMode encoding:NSUTF8StringEncoding]; if ([encodedString length] < 5) { [self printDebugInformation:TXLocalizedStringAlternative([NSBundle bundleForClass:[TPIBlowfishEncryption class]], @"BasicLanguage[1ve-yc]") inChannel:targetChannel]; return; } _callback(encodedString, YES); #undef _callback } - (void)__tpi_decryptMessage:(NSString *)messageBody from:(NSString *)messageFrom target:(NSString *)target decodingCallback:(TLOEncryptionManagerEncodingDecodingCallbackBlock)decodingCallback { #define _callback(_decodedString_, _wasEncrypted_) \ if (decodingCallback) { \ decodingCallback(_decodedString_, _wasEncrypted_); \ } if ([TPIBlowfishEncryption isPluginEnabled] == NO) { [self __tpi_decryptMessage:messageBody from:messageFrom target:target decodingCallback:decodingCallback]; return; } if ([messageBody hasPrefix:@"+OK "] == NO && [messageBody hasPrefix:@"mcps"] == NO) { _callback(messageBody, NO); return; } IRCChannel *targetChannel = nil; if ([self stringIsChannelName:target]) { targetChannel = [self findChannel:target]; } else { targetChannel = [self findChannel:messageFrom]; } if (targetChannel == nil) { _callback(messageBody, NO); return; } NSString *encryptionKey = [TPIBlowfishEncryption encryptionKeyForChannel:targetChannel]; if (encryptionKey == nil) { _callback(messageBody, NO); return; } NSInteger lostBytes = 0; EKBlowfishEncryptionModeOfOperation decodeMode = [TPIBlowfishEncryption encryptionModeOfOperationForChannel:targetChannel]; NSString *decodedString = [EKBlowfishEncryption decodeData:messageBody key:encryptionKey mode:decodeMode encoding:NSUTF8StringEncoding lostBytes:&lostBytes]; if (decodedString == nil) { [self printDebugInformation:TXLocalizedStringAlternative([NSBundle bundleForClass:[TPIBlowfishEncryption class]], @"BasicLanguage[np3-3g]") inChannel:targetChannel]; return; } if (lostBytes > 0) { [self printDebugInformation:TXLocalizedStringAlternative([NSBundle bundleForClass:[TPIBlowfishEncryption class]], @"BasicLanguage[4mm-6d]", lostBytes) inChannel:targetChannel]; /* Do not return for this. This is not a fatal error. */ } _callback(decodedString, YES); #undef _callback } @end #pragma mark - @implementation IRCChannel (IRCChannelSwizzled) + (void)load { XRExchangeInstanceMethod(@"IRCChannel", @"prepareForApplicationTermination", @"__tpi_prepareForApplicationTermination"); XRExchangeInstanceMethod(@"IRCChannel", @"prepareForPermanentDestruction", @"__tpi_prepareForPermanentDestruction"); } - (void)__tpi_destroyEncryptionKeychain { if ([TPIBlowfishEncryption isPluginEnabled] == NO) { return; } if ([self isPrivateMessage] == NO) { return; } [TPIBlowfishEncryption setEncryptionKey:nil forChannel:self]; [TPIBlowfishEncryption setEncryptionModeOfOperation:EKBlowfishEncryptionDefaultModeOfOperation forChannel:self]; } - (void)__tpi_prepareForApplicationTermination { [self __tpi_destroyEncryptionKeychain]; [self __tpi_prepareForApplicationTermination]; } - (void)__tpi_prepareForPermanentDestruction { [self __tpi_destroyEncryptionKeychain]; [self __tpi_prepareForPermanentDestruction]; } @end #pragma mark - @implementation TPCPreferencesUserDefaults (TPIBlowfishEncryptionSwizzledPreferences) + (void)load { XRExchangeInstanceMethod(@"TPCPreferencesUserDefaults", @"objectForKey:", @"__tpi_objectForKey:"); XRExchangeInstanceMethod(@"TPCPreferencesUserDefaults", @"setObject:forKey:", @"__tpi_setObject:forKey:"); } - (void)__tpi_setObject:(id)value forKey:(NSString *)defaultName { if ([TPIBlowfishEncryption isPluginEnabled]) { if ([defaultName hasPrefix:@"Off-the-Record Messaging -> "]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [TDCAlert alertSheetWithWindow:[NSApp keyWindow] body:TXLocalizedStringAlternative([NSBundle bundleForClass:[TPIBlowfishEncryption class]], @"BasicLanguage[alk-c6]") title:TXLocalizedStringAlternative([NSBundle bundleForClass:[TPIBlowfishEncryption class]], @"BasicLanguage[d2x-dh]") defaultButton:TXLocalizedStringAlternative([NSBundle bundleForClass:[TPIBlowfishEncryption class]], @"BasicLanguage[tv1-4l]") alternateButton:nil otherButton:nil]; }); return; // Cancel operation... } } [self __tpi_setObject:value forKey:defaultName]; } - (id)__tpi_objectForKey:(NSString *)defaultName { if ([TPIBlowfishEncryption isPluginEnabled]) { if ([defaultName hasPrefix:@"Off-the-Record Messaging -> "]) { return @(NO); } } return [self __tpi_objectForKey:defaultName]; } @end ================================================ FILE: Sources/Plugins/Blowfish Encryption/LICENSE-GPLv2.txt ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: Sources/Plugins/Blowfish Encryption/LICENSE.txt ================================================ The “Blowfish Encryption Extension” project consists of several components which are distributed under different licenses. Work originating from Codeux Software, LLC falls under the following license declaration. All other work, including license information, can be found by viewing the included ACKNOWLEDGEMENT.txt file. /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ ================================================ FILE: Sources/Plugins/Blowfish Encryption/Resources/Language Files/en.lproj/BasicLanguage.strings ================================================ /* ********************************************************************* * * Copyright (c) 2015 - 2018 Codeux Software, LLC * Please see ACKNOWLEDGEMENT for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of "Codeux Software, LLC", nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "sd4-2b" = "No encryption key is set."; "st5-x8" = "Current Encryption Key: %@"; "rhu-sw" = "Encryption started in query. If this query is closed, then the key must be set again."; "lo0-tu" = "Key exchange failed: corrupted data"; "wo2-d2" = "Key exchange failed: failure to generate public key."; "tr4-jd" = "Key exchange failed: failure to compute secret from public key."; "jmi-xp" = "Successful key exchange. Messages sent to \002%@\002 will now be encrypted."; "cdb-6z" = "To view the actual key used to encrypt these messages, use the \002key\002 command."; "7sz-gy" = "\0034\002Warning:\002\003 If this query is closed, then a new key exchange is required to continue the encrypted chat."; "2kj-i5" = "Key exchange is for queries only."; "wi6-cy" = "A key exchange request already exists for \002%@\002. Please wait for the current one to expire before requesting a new one."; "8nu-ow" = "Received key exchange request from \002%@\002. Processing…"; "ezp-nq" = "Key exchange request has been sent to \002%@\002."; "c3y-2b" = "Key exchange request for \002%@\002 has expired."; "184-19" = "Key exchange response has been sent to \002%@\002."; "eqo-bn" = "Received key exchange response from \002%@\002. Processing…"; "5p8-18" = "The Electronic Codebook (ECB) mode of operation is currently in use for encryption."; "3px-4w" = "The Cipher-block Chaining (CBC) mode of operation is currently in use for encryption."; "9eq-sn" = "Cannot process key exchange request from \002%@\002 until the active encryption keys are reset. This task can be accomplished by using the \002delkey\002 command."; "2oq-wt" = "Cannot process key exchange request for \002%@\002 until the active encryption keys are reset. This task can be accomplished by using the \002delkey\002 command."; "vph-s9" = "The Cipher-block Chaining (CBC) mode of operation will be used for encryption."; "5e8-56" = "The Electronic Codebook (ECB) mode of operation will be used for encryption."; "np3-3g" = "Decryption failed"; "1ve-yc" = "Encryption of message failed; not sending to server."; "9k1-jc" = "Encryption key changed; adjusting accordingly."; "1dv-qi" = "Encryption started"; "ime-kt" = "Encryption stopped"; "4mm-6d" = "\0034\002Decryption lost data!\002\003 When decrypting message, %ld bytes were found at the end of the message which do not form a complete 8 byte encryption block. These bytes were removed and could not be decrypted."; "fmt-65" = "The key you entered is longer than 56 bytes. The Blowfish block cipher has a maximum key length of 56 bytes. \002Your key will be truncated.\002"; "8o8-tj" = "Blowfish Encryption"; "0h8-o3" = "Restart required"; "ei0-4a" = "Please restart Textual to apply changes"; "4qo-gl" = "OK"; "qb6-7a" = "Services not enabled"; "87t-ge" = "In order to use the command “%@“, “FiSH” encryption must be enabled by browsing to Preferences ➜ Addons ➜ Blowfish Encryption"; "oyj-zy" = "OK"; "d2x-dh" = "Cannot toggle preference"; "alk-c6" = "Changes to preferences related to Off-the-Record Messaging (OTR) will have no effect when “FiSH” encryption is enabled. "; "tv1-4l" = "OK"; ================================================ FILE: Sources/Plugins/Blowfish Encryption/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.18 NSHumanReadableCopyright Copyright © 2015 - 2020 Codeux Software, LLC. All rights reserved. MinimumTextualVersion 7.2.4 NSPrincipalClass TPIBlowfishEncryption ================================================ FILE: Sources/Plugins/Blowfish Encryption/Resources/User Interface/en.lproj/TPIBlowfishEncryption.xib ================================================ “FiSH” refers to a dated implementation of the Blowfish block cipher which is popular for encrypting the contents of chatrooms. ================================================ FILE: Sources/Plugins/Caffeine/Caffeine Extension.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C36AAF420F27E9B007CA939 /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAF220F27E9B007CA939 /* BasicLanguage.strings */; }; 4C36AAF720F27EA3007CA939 /* TPI_Caffeine.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAF520F27EA3007CA939 /* TPI_Caffeine.xib */; }; 4C46A15320EC6F2600094EA4 /* TPI_Caffeine.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A15020EC6F2600094EA4 /* TPI_Caffeine.m */; }; 4CBE7B591A91485C008FB230 /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CBE7B341A913F34008FB230 /* CocoaExtensions.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C36AAF320F27E9B007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BasicLanguage.strings; sourceTree = ""; }; 4C36AAF620F27EA3007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TPI_Caffeine.xib; sourceTree = ""; }; 4C46A14F20EC6F2600094EA4 /* TPI_Caffeine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_Caffeine.h; sourceTree = ""; }; 4C46A15020EC6F2600094EA4 /* TPI_Caffeine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_Caffeine.m; sourceTree = ""; }; 4C46A15B20EC6F3100094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A15C20EC6F3100094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A16020EC6F3100094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A16C20EC6F3100094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A16D20EC6F3100094EA4 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4C46A16E20EC6F3100094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A17020EC6F3100094EA4 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4C46A17320EC6F3100094EA4 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4C46A17F20EC6F3100094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A18020EC6F3100094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A18420EC6F3100094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A1AD20EC701B00094EA4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4CBE7B341A913F34008FB230 /* CocoaExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaExtensions.framework; path = "../../../.tmp/SharedBuildResults-Frameworks/CocoaExtensions.framework"; sourceTree = ""; }; 8D576316048677EA00EA77CD /* Caffeine.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Caffeine.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4CBE7B591A91485C008FB230 /* CocoaExtensions.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* BragSpam */ = { isa = PBXGroup; children = ( 4C46A14E20EC6F2600094EA4 /* Classes */, 4C46A14820EC6F2600094EA4 /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = BragSpam; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( 4CBE7B341A913F34008FB230 /* CocoaExtensions.framework */, ); name = Frameworks; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* Caffeine.bundle */, ); name = Products; sourceTree = ""; }; 4C46A14820EC6F2600094EA4 /* Resources */ = { isa = PBXGroup; children = ( 4C46A15420EC6F3100094EA4 /* Configurations */, 4C46A14920EC6F2600094EA4 /* Property Lists */, 4C46A14A20EC6F2600094EA4 /* Language Files */, 4C46A14C20EC6F2600094EA4 /* User Interface */, ); path = Resources; sourceTree = ""; }; 4C46A14920EC6F2600094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A1AD20EC701B00094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A14A20EC6F2600094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C36AAF220F27E9B007CA939 /* BasicLanguage.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C46A14C20EC6F2600094EA4 /* User Interface */ = { isa = PBXGroup; children = ( 4C36AAF520F27EA3007CA939 /* TPI_Caffeine.xib */, ); path = "User Interface"; sourceTree = ""; }; 4C46A14E20EC6F2600094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A14F20EC6F2600094EA4 /* TPI_Caffeine.h */, 4C46A15020EC6F2600094EA4 /* TPI_Caffeine.m */, ); path = Classes; sourceTree = ""; }; 4C46A15420EC6F3100094EA4 /* Configurations */ = { isa = PBXGroup; children = ( 4C46A15820EC6F3100094EA4 /* Build */, ); name = Configurations; path = ../../../Configurations; sourceTree = SOURCE_ROOT; }; 4C46A15820EC6F3100094EA4 /* Build */ = { isa = PBXGroup; children = ( 4C46A16A20EC6F3100094EA4 /* Common */, 4C46A17D20EC6F3100094EA4 /* Debug */, 4C46A15920EC6F3100094EA4 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4C46A15920EC6F3100094EA4 /* Standard Release */ = { isa = PBXGroup; children = ( 4C46A16020EC6F3100094EA4 /* Enabled Features.xcconfig */, 4C46A15C20EC6F3100094EA4 /* Textual Extensions.xcconfig */, 4C46A15B20EC6F3100094EA4 /* Textual.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4C46A16A20EC6F3100094EA4 /* Common */ = { isa = PBXGroup; children = ( 4C46A17320EC6F3100094EA4 /* Foundation Debug.xcconfig */, 4C46A17020EC6F3100094EA4 /* Foundation.xcconfig */, 4C46A16D20EC6F3100094EA4 /* Preserve Symbols.xcconfig */, 4C46A16E20EC6F3100094EA4 /* Textual Extensions.xcconfig */, 4C46A16C20EC6F3100094EA4 /* Textual.xcconfig */, ); path = Common; sourceTree = ""; }; 4C46A17D20EC6F3100094EA4 /* Debug */ = { isa = PBXGroup; children = ( 4C46A18420EC6F3100094EA4 /* Enabled Features.xcconfig */, 4C46A18020EC6F3100094EA4 /* Textual Extensions.xcconfig */, 4C46A17F20EC6F3100094EA4 /* Textual.xcconfig */, ); path = Debug; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* Caffeine Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Caffeine Extension" */; buildPhases = ( 8D57630F048677EA00EA77CD /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "Caffeine Extension"; productInstallPath = "$(HOME)/Library/Bundles"; productName = BragSpam; productReference = 8D576316048677EA00EA77CD /* Caffeine.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; TargetAttributes = { 8D57630D048677EA00EA77CD = { DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Caffeine Extension" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( en, Base, ); mainGroup = 089C166AFE841209C02AAC07 /* BragSpam */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* Caffeine Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 8D57630F048677EA00EA77CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C36AAF720F27EA3007CA939 /* TPI_Caffeine.xib in Resources */, 4C36AAF420F27E9B007CA939 /* BasicLanguage.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A15320EC6F2600094EA4 /* TPI_Caffeine.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C36AAF220F27E9B007CA939 /* BasicLanguage.strings */ = { isa = PBXVariantGroup; children = ( 4C36AAF320F27E9B007CA939 /* en */, ); name = BasicLanguage.strings; sourceTree = ""; }; 4C36AAF520F27EA3007CA939 /* TPI_Caffeine.xib */ = { isa = PBXVariantGroup; children = ( 4C36AAF620F27EA3007CA939 /* en */, ); name = TPI_Caffeine.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1DEB911B08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-Caffeine-bundled"; PRODUCT_NAME = Caffeine; }; name = Debug; }; 1DEB911C08733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-Caffeine-bundled"; PRODUCT_NAME = Caffeine; }; name = Release; }; 1DEB911F08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C46A18020EC6F3100094EA4 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Debug; }; 1DEB912008733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C46A15C20EC6F3100094EA4 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4CCD21E41FA41DE20056F5FE /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (App Store)"; }; 4CCD21E51FA41DE20056F5FE /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-Caffeine-bundled"; PRODUCT_NAME = Caffeine; }; name = "Release (App Store)"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Caffeine Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911B08733D790010E9CD /* Debug */, 1DEB911C08733D790010E9CD /* Release */, 4CCD21E51FA41DE20056F5FE /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Caffeine Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911F08733D790010E9CD /* Debug */, 1DEB912008733D790010E9CD /* Release */, 4CCD21E41FA41DE20056F5FE /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Plugins/Caffeine/Caffeine Extension.xcodeproj/xcshareddata/xcschemes/Caffeine Extension.xcscheme ================================================ ================================================ FILE: Sources/Plugins/Caffeine/Classes/TPI_Caffeine.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_Caffeine : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Caffeine/Classes/TPI_Caffeine.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPI_Caffeine.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_Caffeine () @property (nonatomic, strong) NSMutableArray *observedClients; @property (nonatomic, strong) NSProgress *progressObject; @property (nonatomic, strong) IBOutlet NSView *preferencesPaneView; @end @implementation TPI_Caffeine #pragma mark - #pragma mark Progress Management - (BOOL)disableSleepModeWhenConnected { return [RZUserDefaults() boolForKey:@"Private Extension Store -> Caffeine Extension -> Prevent Sleep"]; } - (void)disableSleepMode { self.progressObject = [RZProcessInfo() beginActivityWithOptions:NSActivityUserInitiated reason:@"Disable sleep mode"]; LogToConsoleWithSubsystem(THOPluginLoggingSubsystem(), "Disabled sleep mode"); } - (void)enableSleepMode { if (self.progressObject == nil) { return; } [RZProcessInfo() endActivity:self.progressObject]; self.progressObject = nil; LogToConsoleWithSubsystem(THOPluginLoggingSubsystem(), "Enabled sleep mode"); } - (void)toggleSleepMode { BOOL oneClientLoggedIn = NO; @synchronized(self.observedClients) { for (IRCClient *client in self.observedClients) { if (client.isLoggedIn == NO) { continue; } oneClientLoggedIn = YES; break; } } if (oneClientLoggedIn) { [self disableSleepMode]; } else { [self enableSleepMode]; } } #pragma mark - #pragma mark Key-value Observation - (void)rebuildObservedClients { /* This method is called anytime the client list changes which means we only begin observing clients here if they are not already observed. */ /* This method will remove observed clients if the user has disabled the option to disable sleep mode. If the option is enabled, then this method observes clients and toggles sleep mode once completed. */ @synchronized(self.observedClients) { BOOL observeClients = [self disableSleepModeWhenConnected]; NSArray *clientList = worldController().clientList; for (IRCClient *client in self.observedClients) { /* Only stop observing client if they are still in -clientList */ /* If they are not, then we are only dangling a reference to a client that no longer exists and can free it below. */ if ([clientList containsObject:client] || observeClients == NO) { @try { [client removeObserver:self forKeyPath:@"isLoggedIn"]; } @catch (NSException *exception) { LogToConsoleFaultWithSubsystem(THOPluginLoggingSubsystem(), "Caught exception: %{public}@", [exception reason]); LogStackTraceWithSubsystem(THOPluginLoggingSubsystem()); } } } if (observeClients == NO) { self.observedClients = nil; } else { if (self.observedClients == nil) { self.observedClients = [NSMutableArray array]; } for (IRCClient *client in clientList) { if ([self.observedClients containsObject:client] == NO) { [self.observedClients addObject:client]; [client addObserver:self forKeyPath:@"isLoggedIn" options:NSKeyValueObservingOptionNew context:NULL]; } } } [self toggleSleepMode]; } } #ifdef TXSystemIsOSXSierraOrLater - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context #else - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context #endif { [self toggleSleepMode]; } - (IBAction)toggledDisableSleepModeWhileConnected:(id)sender { [self rebuildObservedClients]; } #pragma mark - #pragma mark Plugin API - (void)pluginLoadedIntoMemory { /* This plugin uses NSProgress which is not available on Mountain Lion */ /* The Textual Extras installer can detect operating system and will not allow it to be installed on Mountain Lion, but still good to have some type of sanity type. */ if (TEXTUAL_RUNNING_ON(10.9, Mavericks) == NO) { return; } /* Load interface and begin observing client list changes */ (void)[TPIBundleFromClass() loadNibNamed:@"TPI_Caffeine" owner:self topLevelObjects:nil]; [RZNotificationCenter() addObserverForName:IRCWorldClientListWasModifiedNotification object:nil queue:nil usingBlock:^(NSNotification *note) { [self rebuildObservedClients]; }]; /* Plugins are loaded after clients have been setup which means we have to manually build observes in addition to observing the client list change notification. */ [self rebuildObservedClients]; } - (void)pluginWillBeUnloadedFromMemory { [RZNotificationCenter() removeObserver:self]; [self enableSleepMode]; } - (NSString *)pluginPreferencesPaneMenuItemName { return TPILocalizedString(@"BasicLanguage[xqp-6g]"); } - (NSView *)pluginPreferencesPaneView { return self.preferencesPaneView; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Caffeine/Resources/Language Files/en.lproj/BasicLanguage.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "xqp-6g" = "Sleep Mode Management"; ================================================ FILE: Sources/Plugins/Caffeine/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.8 NSHumanReadableCopyright Copyright © 2015 - 2020 Codeux Software, LLC. All rights reserved. MinimumTextualVersion 7.2.4 NSPrincipalClass TPI_Caffeine ================================================ FILE: Sources/Plugins/Caffeine/Resources/User Interface/en.lproj/TPI_Caffeine.xib ================================================ ================================================ FILE: Sources/Plugins/Chat Filter/Chat Filter Extension.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C36AAE920F27E05007CA939 /* TPI_ChatFilterEditFilterSheet.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAE320F27E05007CA939 /* TPI_ChatFilterEditFilterSheet.strings */; }; 4C36AAEA20F27E05007CA939 /* TPI_ChatFilterLogic.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAE520F27E05007CA939 /* TPI_ChatFilterLogic.strings */; }; 4C36AAEB20F27E05007CA939 /* TPI_ChatFilterExtension.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAE720F27E05007CA939 /* TPI_ChatFilterExtension.strings */; }; 4C36AAF020F27E10007CA939 /* TPI_ChatFilterEditFilterSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAEC20F27E10007CA939 /* TPI_ChatFilterEditFilterSheet.xib */; }; 4C36AAF120F27E10007CA939 /* TPI_ChatFilterExtension.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAEE20F27E10007CA939 /* TPI_ChatFilterExtension.xib */; }; 4C46A0AE20EC6B5A00094EA4 /* TPI_ChatFilterLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A0A320EC6B5A00094EA4 /* TPI_ChatFilterLogic.m */; }; 4C46A0AF20EC6B5A00094EA4 /* TPI_ChatFilterEditFilterSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A0A520EC6B5A00094EA4 /* TPI_ChatFilterEditFilterSheet.m */; }; 4C46A0B020EC6B5A00094EA4 /* TPI_ChatFilterExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A0A620EC6B5A00094EA4 /* TPI_ChatFilterExtension.m */; }; 4C46A0B120EC6B5A00094EA4 /* TPI_ChatFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A0A920EC6B5A00094EA4 /* TPI_ChatFilter.m */; }; 4C46A0B220EC6B5A00094EA4 /* TPI_NumberOnlyTextFieldFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A0AB20EC6B5A00094EA4 /* TPI_NumberOnlyTextFieldFormatter.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C1D108820EBF3C000E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D108920EBF3C000E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D108D20EBF3C000E72D80 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C1D109B20EBF3C000E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D109C20EBF3C000E72D80 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4C1D109D20EBF3C000E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D109F20EBF3C000E72D80 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4C1D10A220EBF3C000E72D80 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4C1D10B020EBF3C000E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D10B120EBF3C000E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D10B520EBF3C000E72D80 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C36AAE420F27E05007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TPI_ChatFilterEditFilterSheet.strings; sourceTree = ""; }; 4C36AAE620F27E05007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TPI_ChatFilterLogic.strings; sourceTree = ""; }; 4C36AAE820F27E05007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/TPI_ChatFilterExtension.strings; sourceTree = ""; }; 4C36AAED20F27E10007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TPI_ChatFilterEditFilterSheet.xib; sourceTree = ""; }; 4C36AAEF20F27E10007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TPI_ChatFilterExtension.xib; sourceTree = ""; }; 4C46A0A320EC6B5A00094EA4 /* TPI_ChatFilterLogic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_ChatFilterLogic.m; sourceTree = ""; }; 4C46A0A420EC6B5A00094EA4 /* TPI_ChatFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_ChatFilter.h; sourceTree = ""; }; 4C46A0A520EC6B5A00094EA4 /* TPI_ChatFilterEditFilterSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_ChatFilterEditFilterSheet.m; sourceTree = ""; }; 4C46A0A620EC6B5A00094EA4 /* TPI_ChatFilterExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_ChatFilterExtension.m; sourceTree = ""; }; 4C46A0A720EC6B5A00094EA4 /* TPI_NumberOnlyTextFieldFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_NumberOnlyTextFieldFormatter.h; sourceTree = ""; }; 4C46A0A820EC6B5A00094EA4 /* TPI_ChatFilterInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_ChatFilterInternal.h; sourceTree = ""; }; 4C46A0A920EC6B5A00094EA4 /* TPI_ChatFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_ChatFilter.m; sourceTree = ""; }; 4C46A0AA20EC6B5A00094EA4 /* TPI_ChatFilterLogic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_ChatFilterLogic.h; sourceTree = ""; }; 4C46A0AB20EC6B5A00094EA4 /* TPI_NumberOnlyTextFieldFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_NumberOnlyTextFieldFormatter.m; sourceTree = ""; }; 4C46A0AC20EC6B5A00094EA4 /* TPI_ChatFilterExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_ChatFilterExtension.h; sourceTree = ""; }; 4C46A0AD20EC6B5A00094EA4 /* TPI_ChatFilterEditFilterSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_ChatFilterEditFilterSheet.h; sourceTree = ""; }; 4C46A0B420EC6B6A00094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8D576316048677EA00EA77CD /* Chat Filters.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Chat Filters.bundle"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* PreferencePaneExample */ = { isa = PBXGroup; children = ( 4C46A0A220EC6B5A00094EA4 /* Classes */, 089C167CFE841241C02AAC07 /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = PreferencePaneExample; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; 089C167CFE841241C02AAC07 /* Resources */ = { isa = PBXGroup; children = ( 4CAA730C1BA9B06D00A38456 /* Configurations */, 4C46A0B520EC6B6A00094EA4 /* Language Files */, 4C46A0B320EC6B6A00094EA4 /* Property Lists */, 4C46A0BC20EC6B6A00094EA4 /* User Interface */, ); path = Resources; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* Chat Filters.bundle */, ); name = Products; sourceTree = ""; }; 4C1D108420EBF3C000E72D80 /* Build */ = { isa = PBXGroup; children = ( 4C1D109820EBF3C000E72D80 /* Common */, 4C1D10AD20EBF3C000E72D80 /* Debug */, 4C1D108520EBF3C000E72D80 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4C1D108520EBF3C000E72D80 /* Standard Release */ = { isa = PBXGroup; children = ( 4C1D108D20EBF3C000E72D80 /* Enabled Features.xcconfig */, 4C1D108920EBF3C000E72D80 /* Textual Extensions.xcconfig */, 4C1D108820EBF3C000E72D80 /* Textual.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4C1D109820EBF3C000E72D80 /* Common */ = { isa = PBXGroup; children = ( 4C1D10A220EBF3C000E72D80 /* Foundation Debug.xcconfig */, 4C1D109F20EBF3C000E72D80 /* Foundation.xcconfig */, 4C1D109C20EBF3C000E72D80 /* Preserve Symbols.xcconfig */, 4C1D109D20EBF3C000E72D80 /* Textual Extensions.xcconfig */, 4C1D109B20EBF3C000E72D80 /* Textual.xcconfig */, ); path = Common; sourceTree = ""; }; 4C1D10AD20EBF3C000E72D80 /* Debug */ = { isa = PBXGroup; children = ( 4C1D10B520EBF3C000E72D80 /* Enabled Features.xcconfig */, 4C1D10B120EBF3C000E72D80 /* Textual Extensions.xcconfig */, 4C1D10B020EBF3C000E72D80 /* Textual.xcconfig */, ); path = Debug; sourceTree = ""; }; 4C46A0A220EC6B5A00094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A0A420EC6B5A00094EA4 /* TPI_ChatFilter.h */, 4C46A0A920EC6B5A00094EA4 /* TPI_ChatFilter.m */, 4C46A0AD20EC6B5A00094EA4 /* TPI_ChatFilterEditFilterSheet.h */, 4C46A0A520EC6B5A00094EA4 /* TPI_ChatFilterEditFilterSheet.m */, 4C46A0AC20EC6B5A00094EA4 /* TPI_ChatFilterExtension.h */, 4C46A0A620EC6B5A00094EA4 /* TPI_ChatFilterExtension.m */, 4C46A0A820EC6B5A00094EA4 /* TPI_ChatFilterInternal.h */, 4C46A0AA20EC6B5A00094EA4 /* TPI_ChatFilterLogic.h */, 4C46A0A320EC6B5A00094EA4 /* TPI_ChatFilterLogic.m */, 4C46A0A720EC6B5A00094EA4 /* TPI_NumberOnlyTextFieldFormatter.h */, 4C46A0AB20EC6B5A00094EA4 /* TPI_NumberOnlyTextFieldFormatter.m */, ); path = Classes; sourceTree = ""; }; 4C46A0B320EC6B6A00094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A0B420EC6B6A00094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A0B520EC6B6A00094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C36AAE320F27E05007CA939 /* TPI_ChatFilterEditFilterSheet.strings */, 4C36AAE720F27E05007CA939 /* TPI_ChatFilterExtension.strings */, 4C36AAE520F27E05007CA939 /* TPI_ChatFilterLogic.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C46A0BC20EC6B6A00094EA4 /* User Interface */ = { isa = PBXGroup; children = ( 4C36AAEC20F27E10007CA939 /* TPI_ChatFilterEditFilterSheet.xib */, 4C36AAEE20F27E10007CA939 /* TPI_ChatFilterExtension.xib */, ); path = "User Interface"; sourceTree = ""; }; 4CAA730C1BA9B06D00A38456 /* Configurations */ = { isa = PBXGroup; children = ( 4C1D108420EBF3C000E72D80 /* Build */, ); name = Configurations; path = ../../../Configurations; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* Chat Filter Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Chat Filter Extension" */; buildPhases = ( 8D57630F048677EA00EA77CD /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "Chat Filter Extension"; productInstallPath = "$(HOME)/Library/Bundles"; productName = PreferencePaneExample; productReference = 8D576316048677EA00EA77CD /* Chat Filters.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; TargetAttributes = { 8D57630D048677EA00EA77CD = { DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Chat Filter Extension" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( en, Base, ); mainGroup = 089C166AFE841209C02AAC07 /* PreferencePaneExample */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* Chat Filter Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 8D57630F048677EA00EA77CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C36AAEB20F27E05007CA939 /* TPI_ChatFilterExtension.strings in Resources */, 4C36AAF120F27E10007CA939 /* TPI_ChatFilterExtension.xib in Resources */, 4C36AAE920F27E05007CA939 /* TPI_ChatFilterEditFilterSheet.strings in Resources */, 4C36AAEA20F27E05007CA939 /* TPI_ChatFilterLogic.strings in Resources */, 4C36AAF020F27E10007CA939 /* TPI_ChatFilterEditFilterSheet.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A0B220EC6B5A00094EA4 /* TPI_NumberOnlyTextFieldFormatter.m in Sources */, 4C46A0AE20EC6B5A00094EA4 /* TPI_ChatFilterLogic.m in Sources */, 4C46A0B120EC6B5A00094EA4 /* TPI_ChatFilter.m in Sources */, 4C46A0AF20EC6B5A00094EA4 /* TPI_ChatFilterEditFilterSheet.m in Sources */, 4C46A0B020EC6B5A00094EA4 /* TPI_ChatFilterExtension.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C36AAE320F27E05007CA939 /* TPI_ChatFilterEditFilterSheet.strings */ = { isa = PBXVariantGroup; children = ( 4C36AAE420F27E05007CA939 /* en */, ); name = TPI_ChatFilterEditFilterSheet.strings; sourceTree = ""; }; 4C36AAE520F27E05007CA939 /* TPI_ChatFilterLogic.strings */ = { isa = PBXVariantGroup; children = ( 4C36AAE620F27E05007CA939 /* en */, ); name = TPI_ChatFilterLogic.strings; sourceTree = ""; }; 4C36AAE720F27E05007CA939 /* TPI_ChatFilterExtension.strings */ = { isa = PBXVariantGroup; children = ( 4C36AAE820F27E05007CA939 /* en */, ); name = TPI_ChatFilterExtension.strings; sourceTree = ""; }; 4C36AAEC20F27E10007CA939 /* TPI_ChatFilterEditFilterSheet.xib */ = { isa = PBXVariantGroup; children = ( 4C36AAED20F27E10007CA939 /* en */, ); name = TPI_ChatFilterEditFilterSheet.xib; sourceTree = ""; }; 4C36AAEE20F27E10007CA939 /* TPI_ChatFilterExtension.xib */ = { isa = PBXVariantGroup; children = ( 4C36AAEF20F27E10007CA939 /* en */, ); name = TPI_ChatFilterExtension.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1DEB911C08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-ChatFilterExtension"; PRODUCT_NAME = "Chat Filters"; }; name = Debug; }; 1DEB912008733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C1D10B120EBF3C000E72D80 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Debug; }; 4CCD21EC1FA41E250056F5FE /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C1D108920EBF3C000E72D80 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4CCD21ED1FA41E250056F5FE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-ChatFilterExtension"; PRODUCT_NAME = "Chat Filters"; }; name = Release; }; 4CCD21EE1FA41E390056F5FE /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (App Store)"; }; 4CCD21EF1FA41E390056F5FE /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-ChatFilterExtension"; PRODUCT_NAME = "Chat Filters"; }; name = "Release (App Store)"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Chat Filter Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911C08733D790010E9CD /* Debug */, 4CCD21ED1FA41E250056F5FE /* Release */, 4CCD21EF1FA41E390056F5FE /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Chat Filter Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB912008733D790010E9CD /* Debug */, 4CCD21EC1FA41E250056F5FE /* Release */, 4CCD21EE1FA41E390056F5FE /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilter.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TPI_ChatFilterLimitToValue) { TPI_ChatFilterLimitToValueNoLimit = 0, TPI_ChatFilterLimitToValueChannels = 1, TPI_ChatFilterLimitToValuePrivateMessages = 2, TPI_ChatFilterLimitToValueSpecificItems = 3 }; typedef NS_ENUM(NSUInteger, TPI_ChatFilterAgeComparator) { TPI_ChatFilterAgeComparatorNone = 0, TPI_ChatFilterAgeComparatorLessThan = 1, // Filter only executes if age of user is < limit TPI_ChatFilterAgeComparatorGreaterThan = 2 // Filter only executes if age of user is >= limit }; typedef NS_OPTIONS(NSUInteger, TPI_ChatFilterEventType) { TPI_ChatFilterEventTypeNumeric = 1 << 0, TPI_ChatFilterEventTypePlainTextMessage = 1 << 1, TPI_ChatFilterEventTypeActionMessage = 1 << 2, TPI_ChatFilterEventTypeNoticeMessage = 1 << 3, TPI_ChatFilterEventTypeUserJoinedChannel = 1 << 4, TPI_ChatFilterEventTypeUserLeftChannel = 1 << 5, TPI_ChatFilterEventTypeUserKickedFromChannel = 1 << 6, TPI_ChatFilterEventTypeUserDisconnected = 1 << 7, TPI_ChatFilterEventTypeUserChangedNickname = 1 << 8, TPI_ChatFilterEventTypeChannelTopicReceived = 1 << 9, TPI_ChatFilterEventTypeChannelTopicChanged = 1 << 10, TPI_ChatFilterEventTypeChannelModeReceived = 1 << 11, TPI_ChatFilterEventTypeChannelModeChanged = 1 << 12 }; @interface TPI_ChatFilter : XRPortablePropertyDict @property (readonly) BOOL filterIgnoreContent; @property (readonly) BOOL filterIgnoreOperators; @property (readonly) BOOL filterLogMatch; @property (readonly) BOOL filterLimitedToMyself; @property (readonly) TPI_ChatFilterEventType filterEvents; @property (readonly) TPI_ChatFilterLimitToValue filterLimitedToValue; @property (readonly) TPI_ChatFilterAgeComparator filterAgeComparator; @property (readonly) NSUInteger filterAgeLimit; @property (readonly) NSUInteger filterActionFloodControlInterval; @property (readonly, copy) NSArray *filterLimitedToChannelsIDs; @property (readonly, copy) NSArray *filterLimitedToClientsIDs; @property (readonly, copy) NSArray *filterEventsNumerics; @property (readonly, copy) NSString *filterAction; @property (readonly, copy) NSString *filterDescription; @property (readonly, copy) NSString *filterForwardToDestination; @property (readonly, copy) NSString *filterMatch; @property (readonly, copy) NSString *filterNotes; @property (readonly, copy) NSString *filterSenderMatch; @property (readonly, copy) NSString *filterTitle; @property (readonly, copy) NSString *uniqueIdentifier; - (BOOL)isEventTypeEnabled:(TPI_ChatFilterEventType)eventType; - (BOOL)isCommandEnabled:(NSString *)command; - (nullable instancetype)initWithContentsOfPath:(NSString *)path; - (nullable instancetype)initWithContentsOfURL:(NSURL *)url; - (BOOL)writeToPath:(NSString *)path; - (BOOL)writeToURL:(NSURL *)url; @end #pragma mark - @interface TPI_ChatFilterMutable : TPI_ChatFilter @property (nonatomic, assign, readwrite) BOOL filterIgnoreContent; @property (nonatomic, assign, readwrite) BOOL filterIgnoreOperators; @property (nonatomic, assign, readwrite) BOOL filterLogMatch; @property (nonatomic, assign, readwrite) BOOL filterLimitedToMyself; @property (nonatomic, assign, readwrite) TPI_ChatFilterEventType filterEvents; @property (nonatomic, assign, readwrite) TPI_ChatFilterLimitToValue filterLimitedToValue; @property (nonatomic, assign, readwrite) TPI_ChatFilterAgeComparator filterAgeComparator; @property (nonatomic, assign, readwrite) NSUInteger filterAgeLimit; @property (nonatomic, assign, readwrite) NSUInteger filterActionFloodControlInterval; @property (nonatomic, copy, readwrite) NSArray *filterLimitedToChannelsIDs; @property (nonatomic, copy, readwrite) NSArray *filterLimitedToClientsIDs; @property (nonatomic, copy, readwrite) NSArray *filterEventsNumerics; @property (nonatomic, copy, readwrite) NSString *filterAction; @property (nonatomic, copy, readwrite) NSString *filterForwardToDestination; @property (nonatomic, copy, readwrite) NSString *filterMatch; @property (nonatomic, copy, readwrite) NSString *filterNotes; @property (nonatomic, copy, readwrite) NSString *filterSenderMatch; @property (nonatomic, copy, readwrite) NSString *filterTitle; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilter.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPI_ChatFilter.h" #import "TPI_ChatFilterInternal.h" #import "NSObjectHelperPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TPI_ChatFilter - (void)populateDefaultsPreflight { if (self.initializedAsCopy) { return; } self->_defaults = @{ @"filterEvents" : @(TPI_ChatFilterEventTypePlainTextMessage | TPI_ChatFilterEventTypeActionMessage), @"filterIgnoreContent" : @(NO), @"filterIgnoresOperators" : @(YES), @"filterLimitedToMyself" : @(NO), @"filterLogMatch" : @(NO), @"filterLimitedToValue" : @(TPI_ChatFilterLimitToValueNoLimit), @"filterAgeComparator" : @(TPI_ChatFilterAgeComparatorGreaterThan) }; } - (void)populateDefaultsPostflight { if (self.initializedAsCopy) { return; } SetVariableIfNil(self->_filterLimitedToChannelsIDs, @[]) SetVariableIfNil(self->_filterLimitedToClientsIDs, @[]) SetVariableIfNil(self->_filterEventsNumerics, @[]) SetVariableIfNil(self->_filterAction, @"") SetVariableIfNil(self->_filterForwardToDestination, @"") SetVariableIfNil(self->_filterMatch, @"") SetVariableIfNil(self->_filterNotes, @"") SetVariableIfNil(self->_filterSenderMatch, @"") SetVariableIfNil(self->_filterTitle, @"") SetVariableIfNil(self->_uniqueIdentifier, [NSString stringWithUUID]) } - (instancetype)init { return [self initWithDictionary:@{}]; } - (nullable instancetype)initWithContentsOfPath:(NSString *)path { NSParameterAssert(path != nil); NSURL *url = [NSURL fileURLWithPath:path]; return [self initWithContentsOfURL:url]; } - (nullable instancetype)initWithContentsOfURL:(NSURL *)url { NSParameterAssert(url != nil); NSDictionary *dic = [NSDictionary dictionaryWithContentsOfURL:url]; if (dic == nil) { return nil; } return [self initWithDictionary:dic]; } - (void)populateDictionaryValues:(NSDictionary *)dic { NSParameterAssert(dic != nil); NSMutableDictionary *defaultsMutable = [self->_defaults mutableCopy]; [defaultsMutable addEntriesFromDictionary:dic]; /* Set regular key names */ [defaultsMutable assignArrayTo:&self->_filterLimitedToChannelsIDs forKey:@"filterLimitedToChannelsIDs"]; [defaultsMutable assignArrayTo:&self->_filterLimitedToClientsIDs forKey:@"filterLimitedToClientsIDs"]; [defaultsMutable assignArrayTo:&self->_filterEventsNumerics forKey:@"filterEventsNumerics"]; [defaultsMutable assignBoolTo:&self->_filterIgnoreContent forKey:@"filterIgnoreContent"]; [defaultsMutable assignBoolTo:&self->_filterIgnoreOperators forKey:@"filterIgnoresOperators"]; [defaultsMutable assignBoolTo:&self->_filterLimitedToMyself forKey:@"filterLimitedToMyself"]; [defaultsMutable assignBoolTo:&self->_filterLogMatch forKey:@"filterLogMatch"]; [defaultsMutable assignStringTo:&self->_filterAction forKey:@"filterAction"]; [defaultsMutable assignStringTo:&self->_filterForwardToDestination forKey:@"filterForwardToDestination"]; [defaultsMutable assignStringTo:&self->_filterMatch forKey:@"filterMatch"]; [defaultsMutable assignStringTo:&self->_filterNotes forKey:@"filterNotes"]; [defaultsMutable assignStringTo:&self->_filterSenderMatch forKey:@"filterSenderMatch"]; [defaultsMutable assignStringTo:&self->_filterTitle forKey:@"filterTitle"]; [defaultsMutable assignStringTo:&self->_uniqueIdentifier forKey:@"uniqueIdentifier"]; [defaultsMutable assignUnsignedIntegerTo:&self->_filterActionFloodControlInterval forKey:@"filterActionFloodControlInterval"]; [defaultsMutable assignUnsignedIntegerTo:&self->_filterLimitedToValue forKey:@"filterLimitedToValue"]; [defaultsMutable assignUnsignedIntegerTo:&self->_filterAgeComparator forKey:@"filterAgeComparator"]; [defaultsMutable assignUnsignedIntegerTo:&self->_filterAgeLimit forKey:@"filterAgeLimit"]; /* Maintain backwards compatibility by setting old key names */ /* dic is accessed instead of defaultsMutable because filterEvents will always exist in defaultsMutable */ id filterEvents = dic[@"filterEvents"]; if (filterEvents && [filterEvents isKindOfClass:[NSNumber class]]) { self->_filterEvents = [filterEvents unsignedIntegerValue]; } else { TPI_ChatFilterEventType filterEventsMask = (TPI_ChatFilterEventTypePlainTextMessage | TPI_ChatFilterEventTypeActionMessage); id filterCommandPRIVMSG = defaultsMutable[@"filterCommandPRIVMSG"]; if (filterCommandPRIVMSG && [filterCommandPRIVMSG boolValue] == NO) { filterEventsMask &= ~TPI_ChatFilterEventTypePlainTextMessage; } id filterCommandPRIVMSG_ACTION = defaultsMutable[@"filterCommandPRIVMSG_ACTION"]; if (filterCommandPRIVMSG_ACTION && [filterCommandPRIVMSG_ACTION boolValue] == NO) { filterEventsMask &= ~TPI_ChatFilterEventTypeActionMessage; } if ([defaultsMutable boolForKey:@"filterCommandNOTICE"]) { filterEventsMask |= TPI_ChatFilterEventTypeNoticeMessage; } self->_filterEvents = filterEventsMask; } } - (NSDictionary *)dictionaryValueForTarget:(XRPortablePropertyDictTarget)target { NSMutableDictionary *dic = [NSMutableDictionary dictionary]; /* Maintain backwards compatibility by setting old key names */ [dic setBool:[self isEventTypeEnabled:TPI_ChatFilterEventTypePlainTextMessage] forKey:@"filterCommandPRIVMSG"]; [dic setBool:[self isEventTypeEnabled:TPI_ChatFilterEventTypeActionMessage] forKey:@"filterCommandPRIVMSG_ACTION"]; [dic setBool:[self isEventTypeEnabled:TPI_ChatFilterEventTypeNoticeMessage] forKey:@"filterCommandNOTICE"]; /* Set regular key names */ [dic maybeSetObject:self.filterLimitedToChannelsIDs forKey:@"filterLimitedToChannelsIDs"]; [dic maybeSetObject:self.filterLimitedToClientsIDs forKey:@"filterLimitedToClientsIDs"]; [dic maybeSetObject:self.filterEventsNumerics forKey:@"filterEventsNumerics"]; [dic maybeSetObject:self.filterAction forKey:@"filterAction"]; [dic maybeSetObject:self.filterForwardToDestination forKey:@"filterForwardToDestination"]; [dic maybeSetObject:self.filterMatch forKey:@"filterMatch"]; [dic maybeSetObject:self.filterNotes forKey:@"filterNotes"]; [dic maybeSetObject:self.filterSenderMatch forKey:@"filterSenderMatch"]; [dic maybeSetObject:self.filterTitle forKey:@"filterTitle"]; [dic maybeSetObject:self.uniqueIdentifier forKey:@"uniqueIdentifier"]; [dic setBool:self.filterIgnoreContent forKey:@"filterIgnoreContent"]; [dic setBool:self.filterIgnoreOperators forKey:@"filterIgnoresOperators"]; [dic setBool:self.filterLimitedToMyself forKey:@"filterLimitedToMyself"]; [dic setBool:self.filterLogMatch forKey:@"filterLogMatch"]; [dic setUnsignedInteger:self.filterActionFloodControlInterval forKey:@"filterActionFloodControlInterval"]; [dic setUnsignedInteger:self.filterEvents forKey:@"filterEvents"]; [dic setUnsignedInteger:self.filterLimitedToValue forKey:@"filterLimitedToValue"]; [dic setUnsignedInteger:self.filterAgeComparator forKey:@"filterAgeComparator"]; [dic setUnsignedInteger:self.filterAgeLimit forKey:@"filterAgeLimit"]; if (target == XRPortablePropertyDictTargetCopy || target == XRPortablePropertyDictTargetMutableCopy) { return [dic copy]; } return [dic dictionaryByRemovingDefaults:self->_defaults]; } - (BOOL)isEventTypeEnabled:(TPI_ChatFilterEventType)eventType { return ((self->_filterEvents & eventType) == eventType); } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { TPI_ChatFilter *config = [self allocForCopyAsMutable:mutableCopy]; config->_defaults = self->_defaults; config->_cachedIsCommandEnabledResponses = self->_cachedIsCommandEnabledResponses; return [config initWithDictionary:self.dictionaryValueForCopy]; } - (BOOL)isCommandEnabled:(NSString *)command { NSParameterAssert(command != nil); if (self->_cachedIsCommandEnabledResponses == nil) { self->_cachedIsCommandEnabledResponses = [NSCache new]; } NSNumber *cachedResponse = [self->_cachedIsCommandEnabledResponses objectForKey:command]; if (cachedResponse == nil) { TPI_ChatFilterEventType filterEvents = self.filterEvents; #define _commandMatchesEvent(_command_, _event_) \ if ([command isEqualToString:(_command_)]) { \ cachedResponse = @((filterEvents & _event_) == _event_); \ } _commandMatchesEvent(@"JOIN", TPI_ChatFilterEventTypeUserJoinedChannel) else _commandMatchesEvent(@"PART", TPI_ChatFilterEventTypeUserLeftChannel) else _commandMatchesEvent(@"KICK", TPI_ChatFilterEventTypeUserKickedFromChannel) else _commandMatchesEvent(@"QUIT", TPI_ChatFilterEventTypeUserDisconnected) else _commandMatchesEvent(@"NICK", TPI_ChatFilterEventTypeUserChangedNickname) else _commandMatchesEvent(@"TOPIC", TPI_ChatFilterEventTypeChannelTopicChanged) else _commandMatchesEvent(@"MODE", TPI_ChatFilterEventTypeChannelModeChanged) else _commandMatchesEvent(@"332", TPI_ChatFilterEventTypeChannelTopicReceived) else _commandMatchesEvent(@"333", TPI_ChatFilterEventTypeChannelTopicReceived) else _commandMatchesEvent(@"324", TPI_ChatFilterEventTypeChannelModeReceived) else { cachedResponse = @([self.filterEventsNumerics containsObject:command]); } [self->_cachedIsCommandEnabledResponses setObject:cachedResponse forKey:command]; } return cachedResponse.boolValue; #undef _commandMatchesEvent } - (void)purgeIsCommandEnabledResponses { if (self->_cachedIsCommandEnabledResponses == nil) { return; } [self->_cachedIsCommandEnabledResponses removeAllObjects]; } - (NSString *)filterDescription { return TPILocalizedString(@"TPI_ChatFilterExtension[dka-bx]", self.filterTitle); } - (__kindof XRPortablePropertyDict *)mutableClass { return [TPI_ChatFilterMutable self]; } - (BOOL)writeToPath:(NSString *)path { NSParameterAssert(path != nil); NSURL *url = [NSURL fileURLWithPath:path]; return [self writeToURL:url]; } - (BOOL)writeToURL:(NSURL *)url { NSParameterAssert(url != nil); NSDictionary *dictionaryValue = self.dictionaryValue; NSError *parseError = nil; NSData *propertyList = [NSPropertyListSerialization dataWithPropertyList:dictionaryValue format:NSPropertyListBinaryFormat_v1_0 options:0 error:&parseError]; if (propertyList == nil) { if (parseError) { LogToConsoleErrorWithSubsystem(THOPluginLoggingSubsystem(), "Error Creating Property List: %@", parseError.localizedDescription); } return NO; } BOOL writeResult = [propertyList writeToURL:url atomically:YES]; if (writeResult == NO) { LogToConsoleErrorWithSubsystem(THOPluginLoggingSubsystem(), "Write failed"); return NO; } return YES; } @end #pragma mark - @implementation TPI_ChatFilterMutable @dynamic filterAgeComparator; @dynamic filterAgeLimit; @dynamic filterIgnoreContent; @dynamic filterIgnoreOperators; @dynamic filterLogMatch; @dynamic filterLimitedToMyself; @dynamic filterEvents; @dynamic filterLimitedToValue; @dynamic filterLimitedToChannelsIDs; @dynamic filterLimitedToClientsIDs; @dynamic filterEventsNumerics; @dynamic filterActionFloodControlInterval; @dynamic filterAction; @dynamic filterForwardToDestination; @dynamic filterMatch; @dynamic filterNotes; @dynamic filterSenderMatch; @dynamic filterTitle; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyDict *)immutableClass { return [TPI_ChatFilter self]; } - (void)setFilterIgnoreContent:(BOOL)filterIgnoreContent { if (self->_filterIgnoreContent != filterIgnoreContent) { self->_filterIgnoreContent = filterIgnoreContent; } } - (void)setFilterIgnoreOperators:(BOOL)filterIgnoreOperators { if (self->_filterIgnoreOperators != filterIgnoreOperators) { self->_filterIgnoreOperators = filterIgnoreOperators; } } - (void)setFilterLogMatch:(BOOL)filterLogMatch { if (self->_filterLogMatch != filterLogMatch) { self->_filterLogMatch = filterLogMatch; } } - (void)setFilterLimitedToMyself:(BOOL)filterLimitedToMyself { if (self->_filterLimitedToMyself != filterLimitedToMyself) { self->_filterLimitedToMyself = filterLimitedToMyself; } } - (void)setFilterEvents:(TPI_ChatFilterEventType)filterEvents { if (self->_filterEvents != filterEvents) { self->_filterEvents = filterEvents; [self purgeIsCommandEnabledResponses]; } } - (void)setFilterLimitedToValue:(TPI_ChatFilterLimitToValue)filterLimitedToValue { if (self->_filterLimitedToValue != filterLimitedToValue) { self->_filterLimitedToValue = filterLimitedToValue; } } - (void)setFilterLimitedToChannelsIDs:(NSArray *)filterLimitedToChannelsIDs { NSParameterAssert(filterLimitedToChannelsIDs != nil); if (self->_filterLimitedToChannelsIDs != filterLimitedToChannelsIDs) { self->_filterLimitedToChannelsIDs = [filterLimitedToChannelsIDs copy]; } } - (void)setFilterLimitedToClientsIDs:(NSArray *)filterLimitedToClientsIDs { NSParameterAssert(filterLimitedToClientsIDs != nil); if (self->_filterLimitedToClientsIDs != filterLimitedToClientsIDs) { self->_filterLimitedToClientsIDs = [filterLimitedToClientsIDs copy]; } } - (void)setFilterEventsNumerics:(NSArray *)filterEventsNumerics { NSParameterAssert(filterEventsNumerics != nil); if (self->_filterEventsNumerics != filterEventsNumerics) { self->_filterEventsNumerics = [filterEventsNumerics copy]; [self purgeIsCommandEnabledResponses]; } } - (void)setFilterAgeComparator:(TPI_ChatFilterAgeComparator)filterAgeComparator { if (self->_filterAgeComparator != filterAgeComparator) { self->_filterAgeComparator = filterAgeComparator; } } - (void)setFilterAgeLimit:(NSUInteger)filterAgeLimit { if (self->_filterAgeLimit != filterAgeLimit) { self->_filterAgeLimit = filterAgeLimit; } } - (void)setFilterActionFloodControlInterval:(NSUInteger)filterActionFloodControlInterval { if (self->_filterActionFloodControlInterval != filterActionFloodControlInterval) { self->_filterActionFloodControlInterval = filterActionFloodControlInterval; } } - (void)setFilterAction:(NSString *)filterAction { NSParameterAssert(filterAction != nil); if (self->_filterAction != filterAction) { self->_filterAction = [filterAction copy]; } } - (void)setFilterForwardToDestination:(NSString *)filterForwardToDestination { NSParameterAssert(filterForwardToDestination != nil); if (self->_filterForwardToDestination != filterForwardToDestination) { self->_filterForwardToDestination = [filterForwardToDestination copy]; } } - (void)setFilterMatch:(NSString *)filterMatch { NSParameterAssert(filterMatch != nil); if (self->_filterMatch != filterMatch) { self->_filterMatch = [filterMatch copy]; } } - (void)setFilterNotes:(NSString *)filterNotes { NSParameterAssert(filterNotes != nil); if (self->_filterNotes != filterNotes) { self->_filterNotes = [filterNotes copy]; } } - (void)setFilterSenderMatch:(NSString *)filterSenderMatch { NSParameterAssert(filterSenderMatch != nil); if (self->_filterSenderMatch != filterSenderMatch) { self->_filterSenderMatch = [filterSenderMatch copy]; } } - (void)setFilterTitle:(NSString *)filterTitle { NSParameterAssert(filterTitle != nil); if (self->_filterTitle != filterTitle) { self->_filterTitle = [filterTitle copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilterEditFilterSheet.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" #import "TPI_ChatFilter.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TPI_ChatFilterActionTokenTag) { TPI_ChatFilterActionTokenTagChannelName = 1, TPI_ChatFilterActionTokenTagLocalNickname = 2, TPI_ChatFilterActionTokenTagNetworkName = 3, TPI_ChatFilterActionTokenOriginalMessage = 4, TPI_ChatFilterActionTokenTagSenderNickname = 5, TPI_ChatFilterActionTokenTagSenderAddress = 6, TPI_ChatFilterActionTokenTagSenderUsername = 7, TPI_ChatFilterActionTokenTagSenderHostmask = 8, TPI_ChatFilterActionTokenTagServerAddress = 9 }; @protocol TPI_ChatFilterEditFilterSheetDelegate; @interface TPI_ChatFilterEditFilterSheet : TDCSheetBase - (instancetype)initWithFilter:(nullable TPI_ChatFilter *)filter NS_DESIGNATED_INITIALIZER; - (void)start; @end @protocol TPI_ChatFilterEditFilterSheetDelegate @optional - (void)chatFilterEditFilterSheet:(TPI_ChatFilterEditFilterSheet *)sender onOk:(TPI_ChatFilter *)filter; - (void)chatFilterEditFilterSheetWillClose:(TPI_ChatFilterEditFilterSheet *)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilterEditFilterSheet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPI_ChatFilterEditFilterSheet.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TPI_ChatFilterEditFilterSheetSelection) { TPI_ChatFilterEditFilterSheetSelectionGeneral = 0, TPI_ChatFilterEditFilterSheetSelectionChannels = 1, TPI_ChatFilterEditFilterSheetSelectionEvents = 2, TPI_ChatFilterEditFilterSheetSelectionSender = 3, TPI_ChatFilterEditFilterSheetSelectionNotes = 4, TPI_ChatFilterEditFilterSheetSelectionAdvanced = 5 }; @interface TPI_ChatFilterEditFilterSheet () @property (nonatomic, strong) TPI_ChatFilterMutable *filter; @property (nonatomic, weak) IBOutlet NSTabView *contentViewTabView; @property (nonatomic, weak) IBOutlet NSTextField *filterAgeLimitTextField; @property (nonatomic, weak) IBOutlet NSTextField *filterMatchTextField; @property (nonatomic, weak) IBOutlet NSTextField *filterSenderMatchTextField; @property (nonatomic, weak) IBOutlet NSTextField *filterTitleTextField; @property (nonatomic, weak) IBOutlet NSTextField *filterNotesTextField; @property (nonatomic, weak) IBOutlet NSTextField *filterActionFloodControlIntervalTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *filterEventNumericTextField; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *filterForwardToDestinationTextField; @property (nonatomic, weak) IBOutlet TVCAutoExpandingTokenField *filterActionTokenField; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenChannelName; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenLocalNickname; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenNetworkName; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenOriginalMessage; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenSenderAddress; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenSenderHostmask; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenSenderNickname; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenSenderUsername; @property (nonatomic, weak) IBOutlet NSTokenField *filterActionTokenServerAddress; @property (nonatomic, weak) IBOutlet NSView *filterLimitedToHostView; @property (nonatomic, weak) IBOutlet NSView *filterLimitedToSelectionHostView; @property (nonatomic, weak) IBOutlet NSPopUpButton *filterAgeLimitComparatorButton; @property (nonatomic, weak) IBOutlet NSButton *filterLimitToNoLimitButton; @property (nonatomic, weak) IBOutlet NSButton *filterLimitToOnlyChannelsButton; @property (nonatomic, weak) IBOutlet NSButton *filterLimitToOnlyPrivateMessagesButton; @property (nonatomic, weak) IBOutlet NSButton *filterLimitToSpecificItemsButton; @property (nonatomic, weak) IBOutlet NSButton *filterIgnoreContentCheck; @property (nonatomic, weak) IBOutlet NSButton *filterIgnoreOperatorsCheck; @property (nonatomic, weak) IBOutlet NSButton *filterLogMatchCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventPlainTextMessageCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventActionMessageCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventNoticeMessageCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventUserJoinedChannelCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventUserLeftChannelCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventUserKickedFromChannelCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventUserDisconnectedCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventUserChangedNicknameCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventChannelTopicReceivedCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventChannelTopicChangedCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventChannelModeReceivedCheck; @property (nonatomic, weak) IBOutlet NSButton *filterEventChannelModeChangedCheck; @property (nonatomic, weak) IBOutlet NSButton *filterLimitedToMyselfCheck; @property (nonatomic, assign) BOOL filterIgnoreOperatorsCheckEnabled; @property (nonatomic, copy) NSArray *filterActionAutoCompletedTokens; @property (nonatomic, strong) IBOutlet TVCChannelSelectionViewController *filterLimitToSelectionOutlineView; - (IBAction)viewFilterMatchHelpText:(id)sender; - (IBAction)viewFilterActionHelpText:(id)sender; - (IBAction)viewFilterSenderMatchHelpText:(id)sender; - (IBAction)viewFilterForwardToDestinationHelpText:(id)sender; - (IBAction)filterLimitedToMatrixChanged:(id)sender; - (IBAction)filterIgnoreContentCheckChanged:(id)sender; - (IBAction)filterEventTypeChanged:(id)sender; - (IBAction)filterLimitedToMyselfChanged:(id)sender; @end #pragma mark - @interface TPI_ChatFilterFilterActionToken : NSObject @property (nonatomic, copy) NSString *token; @property (nonatomic, copy, readonly, nullable) NSString *tokenTitle; + (TPI_ChatFilterFilterActionToken *)tokenWithToken:(NSString *)token; + (nullable TPI_ChatFilterFilterActionToken *)tokenWithTokenTitle:(NSString *)tokenTitle; + (NSArray *)tokens; + (NSArray *)tokenTitles; + (nullable NSString *)titleForToken:(NSString *)token; + (BOOL)isToken:(NSString *)token; @end #pragma mark - @implementation TPI_ChatFilterEditFilterSheet #pragma mark - #pragma mark Primary Sheet Structure - (instancetype)initWithFilter:(nullable TPI_ChatFilter *)filter { if ((self = [super initWithWindow:nil])) { if (filter == nil) { self.filter = [TPI_ChatFilterMutable new]; } else { self.filter = [filter mutableCopy]; } [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [TPIBundleFromClass() loadNibNamed:@"TPI_ChatFilterEditFilterSheet" owner:self topLevelObjects:nil]; [self populateTokenFieldStringValues]; [self setupTextFieldRules]; [self loadFilter]; [self updateEnabledStateOfSenderMatch]; [self updateEnabledStateOfFilterEvents]; [self updateEnableStateOfFilterActionTokenField]; [self updateEnabledStateOfComponentsConstrainedByFilterEvents]; [self updateFilterLimitedToMatrix]; [self updateVisibilityOfLimitedToTableHostView]; [self toggleOkButton]; [self.filterLimitToSelectionOutlineView attachToView:self.filterLimitedToSelectionHostView]; } - (void)start { [self startSheet]; } - (void)loadFilter { self.filterMatchTextField.stringValue = self.filter.filterMatch; [self setTokens:self.filter.filterAction inTokenField:self.filterActionTokenField]; [self.filterAgeLimitComparatorButton selectItemWithTag:self.filter.filterAgeComparator]; self.filterAgeLimitTextField.integerValue = self.filter.filterAgeLimit; self.filterActionFloodControlIntervalTextField.integerValue = self.filter.filterActionFloodControlInterval; self.filterTitleTextField.stringValue = self.filter.filterTitle; self.filterNotesTextField.stringValue = self.filter.filterNotes; self.filterSenderMatchTextField.stringValue = self.filter.filterSenderMatch; self.filterForwardToDestinationTextField.stringValue = self.filter.filterForwardToDestination; self.filterIgnoreContentCheck.state = self.filter.filterIgnoreContent; self.filterLogMatchCheck.state = self.filter.filterLogMatch; self.filterLimitedToMyselfCheck.state = self.filter.filterLimitedToMyself; self.filterEventPlainTextMessageCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypePlainTextMessage]; self.filterEventActionMessageCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeActionMessage]; self.filterEventNoticeMessageCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeNoticeMessage]; self.filterEventUserJoinedChannelCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeUserJoinedChannel]; self.filterEventUserLeftChannelCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeUserLeftChannel]; self.filterEventUserKickedFromChannelCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeUserKickedFromChannel]; self.filterEventUserDisconnectedCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeUserDisconnected]; self.filterEventUserChangedNicknameCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeUserChangedNickname]; self.filterEventChannelTopicReceivedCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeChannelTopicReceived]; self.filterEventChannelTopicChangedCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeChannelTopicChanged]; self.filterEventChannelModeReceivedCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeChannelModeReceived]; self.filterEventChannelModeChangedCheck.state = [self.filter isEventTypeEnabled:TPI_ChatFilterEventTypeChannelModeChanged]; NSArray *filterEventsNumerics = self.filter.filterEventsNumerics; if (filterEventsNumerics) { NSString *filterEventsNumericsJoined = [filterEventsNumerics componentsJoinedByString:@", "]; self.filterEventNumericTextField.stringValue = filterEventsNumericsJoined; } else { [self.filterEventNumericTextField performValidation]; } NSArray *filterLimitedToClientsIDs = self.filter.filterLimitedToClientsIDs; NSArray *filterLimitedToChannelsIDs = self.filter.filterLimitedToChannelsIDs; self.filterLimitToSelectionOutlineView.selectedClientIds = filterLimitedToClientsIDs; self.filterLimitToSelectionOutlineView.selectedChannelIds = filterLimitedToChannelsIDs; } - (void)saveFilter { self.filter.filterMatch = self.filterMatchTextField.stringValue; NSString *filterActionStringValue = [self stringValueForTokenField:self.filterActionTokenField]; self.filter.filterAction = filterActionStringValue; self.filter.filterAgeComparator = self.filterAgeLimitComparatorButton.selectedTag; NSInteger filterAgeLimit = self.filterAgeLimitTextField.integerValue; if (filterAgeLimit < 0) { filterAgeLimit = 0; } self.filter.filterAgeLimit = filterAgeLimit; NSInteger filterActionFloodControlInterval = self.filterActionFloodControlIntervalTextField.integerValue; if (filterActionFloodControlInterval < 0) { filterActionFloodControlInterval = 0; } self.filter.filterActionFloodControlInterval = filterActionFloodControlInterval; self.filter.filterTitle = self.filterTitleTextField.stringValue; self.filter.filterNotes = self.filterNotesTextField.stringValue; self.filter.filterSenderMatch = self.filterSenderMatchTextField.stringValue; self.filter.filterForwardToDestination = self.filterForwardToDestinationTextField.stringValue; self.filter.filterIgnoreOperators = (self.filterIgnoreOperatorsCheck.state == NSControlStateValueOn); self.filter.filterIgnoreContent = (self.filterIgnoreContentCheck.state == NSControlStateValueOn); self.filter.filterLogMatch = (self.filterLogMatchCheck.state == NSControlStateValueOn); self.filter.filterLimitedToMyself = (self.filterLimitedToMyselfCheck.state == NSControlStateValueOn); self.filter.filterEvents = [self compileFilterEvents]; self.filter.filterEventsNumerics = [self compileFilterEventsNumericsOrReturnEmptyArray]; self.filter.filterLimitedToClientsIDs = self.filterLimitToSelectionOutlineView.selectedClientIds; self.filter.filterLimitedToChannelsIDs = self.filterLimitToSelectionOutlineView.selectedChannelIds; } - (BOOL)filterIgnoreOperatorsCheckValue { if (self.filterIgnoreOperatorsCheckEnabled == NO) { return NO; } return self.filter.filterIgnoreOperators; } - (void)setFilterIgnoreOperatorsCheckValue:(BOOL)filterIgnoreOperatorsCheckValue { self.filter.filterIgnoreOperators = filterIgnoreOperatorsCheckValue; } - (NSArray *)compileFilterEventsNumericsOrReturnEmptyArray { NSArray *filterEventNumerics = [self compileFilterEventsNumerics]; if (filterEventNumerics == nil) { return @[]; } return filterEventNumerics; } - (nullable NSArray *)compileFilterEventsNumerics { NSString *numericsString = self.filterEventNumericTextField.value; NSArray *numerics = [numericsString componentsSeparatedByString:@","]; NSMutableArray *filterEventsNumerics = nil; // Create later so we don't waste memory if error. for (__strong NSString *numeric in numerics) { numeric = numeric.trim; if (numeric.length == 0) { continue; // Empty segment. We can ignore. } if (numeric.numericOnly) { if (numeric.length > 3) { return nil; // Bad value, fail completely } // Convert to integer and back to remove leading zeros numeric = [NSString stringWithFormat:@"%ld", numeric.integerValue]; } else if (numeric.alphabeticNumericOnly) { if (numeric.length > 20) { return nil; // Bad value, fail completely } numeric = numeric.uppercaseString; } else { return nil; // Bad value, fail completely } if (filterEventsNumerics == nil) { filterEventsNumerics = [NSMutableArray array]; } if ([filterEventsNumerics containsObject:numeric] == NO) { [filterEventsNumerics addObject:numeric]; } } return [filterEventsNumerics copy]; } - (TPI_ChatFilterEventType)compileFilterEvents { TPI_ChatFilterEventType filterEvents = 0; if (self.filterEventPlainTextMessageCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypePlainTextMessage; if (self.filterEventActionMessageCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeActionMessage; if (self.filterEventNoticeMessageCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeNoticeMessage; if (self.filterEventUserJoinedChannelCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeUserJoinedChannel; if (self.filterEventUserLeftChannelCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeUserLeftChannel; if (self.filterEventUserKickedFromChannelCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeUserKickedFromChannel; if (self.filterEventUserDisconnectedCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeUserDisconnected; if (self.filterEventUserChangedNicknameCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeUserChangedNickname; if (self.filterEventChannelTopicReceivedCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeChannelTopicReceived; if (self.filterEventChannelTopicChangedCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeChannelTopicChanged; if (self.filterEventChannelModeReceivedCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeChannelModeReceived; if (self.filterEventChannelModeChangedCheck.state == NSControlStateValueOn) filterEvents |= TPI_ChatFilterEventTypeChannelModeChanged; return filterEvents; } - (void)ok:(id)sender { if ([self okOrError] == NO) { return; } [self saveFilter]; if ([self.delegate respondsToSelector:@selector(chatFilterEditFilterSheet:onOk:)]) { [self.delegate chatFilterEditFilterSheet:self onOk:[self.filter copy]]; } [super ok:nil]; } - (BOOL)okOrError { if ([self okOrErrorForTextField:self.filterEventNumericTextField inSelection:TPI_ChatFilterEditFilterSheetSelectionEvents] == NO) { return NO; } if ([self okOrErrorForTextField:self.filterForwardToDestinationTextField inSelection:TPI_ChatFilterEditFilterSheetSelectionAdvanced] == NO) { return NO; } return YES; } - (BOOL)okOrErrorForTextField:(TVCValidatedTextField *)textField inSelection:(TPI_ChatFilterEditFilterSheetSelection)selection { if (textField.valueIsValid) { return YES; } [self navigateToSelection:selection]; /* Give navigation time to settle before trying to attach popover */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [textField showValidationErrorPopover]; }); return NO; } - (void)navigateToSelection:(TPI_ChatFilterEditFilterSheetSelection)selection { if (self.contentViewTabView.indexOfSelectedItem == selection) { return; } [self.contentViewTabView selectTabViewItemAtIndex:selection]; } - (void)windowWillClose:(NSNotification *)note { [RZNotificationCenter() removeObserver:self]; if ([self.delegate respondsToSelector:@selector(chatFilterEditFilterSheetWillClose:)]) { [self.delegate chatFilterEditFilterSheetWillClose:self]; } } #pragma mark - #pragma mark Token Field Delegate - (void)populateTokenFieldStringValues { NSCharacterSet *emptyCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@""]; self.filterActionTokenField.tokenizingCharacterSet = emptyCharacterSet; self.filterActionTokenField.completionDelay = 0.2; [self setToken:@"%_channelName_%" inTokenField:self.filterActionTokenChannelName]; [self setToken:@"%_localNickname_%" inTokenField:self.filterActionTokenLocalNickname]; [self setToken:@"%_networkName_%" inTokenField:self.filterActionTokenNetworkName]; [self setToken:@"%_originalMessage_%" inTokenField:self.filterActionTokenOriginalMessage]; [self setToken:@"%_senderNickname_%" inTokenField:self.filterActionTokenSenderNickname]; [self setToken:@"%_senderUsername_%" inTokenField:self.filterActionTokenSenderUsername]; [self setToken:@"%_senderAddress_%" inTokenField:self.filterActionTokenSenderAddress]; [self setToken:@"%_senderHostmask_%" inTokenField:self.filterActionTokenSenderHostmask]; [self setToken:@"%_serverAddress_%" inTokenField:self.filterActionTokenServerAddress]; } - (nullable NSArray *)tokenField:(NSTokenField *)tokenField readFromPasteboard:(NSPasteboard *)pboard { NSString *stringContent = pboard.stringContent; return [self tokensFromString:stringContent]; } - (BOOL)tokenField:(NSTokenField *)tokenField writeRepresentedObjects:(NSArray *)objects toPasteboard:(NSPasteboard *)pboard { NSString *stringContent = [objects componentsJoinedByString:@""]; pboard.stringContent = stringContent; return YES; } - (NSTokenStyle)tokenField:(NSTokenField *)tokenField styleForRepresentedObject:(id)representedObject { if ([representedObject isKindOfClass:[TPI_ChatFilterFilterActionToken class]]) { return NSTokenStyleRounded; } return NSTokenStyleNone; } - (nullable NSString *)tokenField:(NSTokenField *)tokenField displayStringForRepresentedObject:(id)representedObject { if ([representedObject isKindOfClass:[TPI_ChatFilterFilterActionToken class]]) { return [representedObject tokenTitle]; } return representedObject; } - (nullable id)tokenField:(NSTokenField *)tokenField representedObjectForEditingString:(NSString *)editingString { if (tokenField == self.filterActionTokenField) { NSArray *tokenTitles = [self.filterActionAutoCompletedTokens filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"SELF beginswith[cd] %@", editingString]]; if (tokenTitles.count > 0) { NSString *tokenTitle = tokenTitles.firstObject; return [TPI_ChatFilterFilterActionToken tokenWithTokenTitle:tokenTitle]; } } return editingString; } - (nullable NSArray *)tokenField:(NSTokenField *)tokenField completionsForSubstring:(NSString *)substring indexOfToken:(NSInteger)tokenIndex indexOfSelectedItem:(nullable NSInteger *)selectedIndex { if (tokenField == self.filterActionTokenField) { NSArray *tokenTitles = [TPI_ChatFilterFilterActionToken tokenTitles]; NSArray *tokenTitlesFiltered = [tokenTitles filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"SELF beginswith[cd] %@", substring]]; self.filterActionAutoCompletedTokens = tokenTitlesFiltered; return tokenTitlesFiltered; } return nil; } - (void)performFilterActionTokenCompletion { } - (NSString *)stringValueForTokenField:(NSTokenField *)tokenField { return [tokenField.objectValue componentsJoinedByString:@""]; } - (void)setTokens:(NSString *)tokens inTokenField:(NSTokenField *)tokenField { NSArray *tokenObjects = [self tokensFromString:tokens]; tokenField.objectValue = tokenObjects; } - (void)setToken:(NSString *)token inTokenField:(NSTokenField *)tokenField { TPI_ChatFilterFilterActionToken *tokenObject = [TPI_ChatFilterFilterActionToken tokenWithToken:token]; tokenField.objectValue = @[tokenObject]; } - (NSArray *)tokensFromString:(nullable NSString *)string { NSString *tokenString = string; if (tokenString == nil) { return @[]; } NSMutableArray *tokens = [NSMutableArray array]; NSInteger currentPosition = 0; NSInteger tokenStringLength = tokenString.length; while (currentPosition < tokenStringLength) { NSRange searchRange = NSMakeRange(currentPosition, (tokenStringLength - currentPosition)); NSRange range = [tokenString rangeOfString:@"%_([a-zA-Z0-9_]+)_%" options:NSRegularExpressionSearch range:searchRange]; if (range.location == NSNotFound) { NSString *tokenStringPrefix = [tokenString substringWithRange:searchRange]; [tokens addObject:tokenStringPrefix]; break; } NSRange tokenStringPrefixRange = NSMakeRange(currentPosition, (range.location - currentPosition)); if (tokenStringPrefixRange.length > 0) { NSString *tokenStringPrefix = [tokenString substringWithRange:tokenStringPrefixRange]; [tokens addObject:tokenStringPrefix]; } NSString *tokenStringToken = [tokenString substringWithRange:range]; if ([TPI_ChatFilterFilterActionToken isToken:tokenStringToken]) { TPI_ChatFilterFilterActionToken *token = [TPI_ChatFilterFilterActionToken tokenWithToken:tokenStringToken]; [tokens addObject:token]; } else { [tokens addObject:tokenStringToken]; } currentPosition = NSMaxRange(range); } if (tokens.count == 0) { [tokens addObject:tokenString]; } return tokens; } #pragma mark - #pragma mark Utilities - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector { if (control != self.filterActionTokenField && control != self.filterNotesTextField) { return NO; } NSRange selectedRange = [textView selectedRange]; if (selectedRange.length > 0) { return NO; } if (commandSelector == @selector(insertNewline:)) { NSRange editedRange = textView.textStorage.editedRange; if (editedRange.length > 1) { return NO; } [textView insertNewlineIgnoringFieldEditor:self]; return YES; } return NO; } - (void)controlTextDidChange:(NSNotification *)notification { [self toggleOkButton]; } - (void)validatedTextFieldTextDidChange:(id)sender { [self toggleOkButton]; } - (void)toggleOkButton { BOOL disabled = NO; if (self.filterTitleTextField.stringValue.length == 0) { disabled = YES; } if (disabled == NO) { if (self.filterIgnoreContentCheck.state == NSControlStateValueOff && self.filterForwardToDestinationTextField.stringValue.length == 0) { if (self.filterActionTokenField.stringValue.length == 0) { disabled = YES; } } } self.okButton.enabled = (disabled == NO); } - (void)setupTextFieldRules { /* "Forward To" text field */ self.filterForwardToDestinationTextField.textDidChangeCallback = self; self.filterForwardToDestinationTextField.performValidationWhenEmpty = NO; self.filterForwardToDestinationTextField.stringValueIsInvalidOnEmpty = NO; self.filterForwardToDestinationTextField.stringValueIsTrimmed = YES; self.filterForwardToDestinationTextField.stringValueUsesOnlyFirstToken = NO; self.filterForwardToDestinationTextField.validationBlock = ^NSString *(NSString *currentValue) { if (currentValue.length > 125) { return TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[m0u-tw]"); } if ([XRRegularExpression string:currentValue isMatchedByRegex:@"^([a-zA-Z0-9\\-\\_\\s]+)$"] == NO) { return TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[5kd-jt]"); } return nil; }; /* "Numerics" text field */ self.filterEventNumericTextField.textDidChangeCallback = self; self.filterEventNumericTextField.performValidationWhenEmpty = NO; self.filterEventNumericTextField.stringValueIsInvalidOnEmpty = NO; self.filterEventNumericTextField.stringValueIsTrimmed = NO; self.filterEventNumericTextField.stringValueUsesOnlyFirstToken = NO; self.filterEventNumericTextField.validationBlock = ^NSString *(NSString *currentValue) { if ([self compileFilterEventsNumerics] == nil) { return TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[9ri-sd]"); } return nil; }; } - (void)updateVisibilityOfLimitedToTableHostView { if (self.filter.filterLimitedToValue == TPI_ChatFilterLimitToValueSpecificItems) { self.filterLimitedToHostView.hidden = NO; } else { self.filterLimitedToHostView.hidden = YES; } } - (void)updateEnableStateOfFilterActionTokenField { } - (void)updateEnabledStateOfFilterEvents { BOOL enabled = (self.filter.filterLimitedToValue != TPI_ChatFilterLimitToValuePrivateMessages); self.filterEventUserJoinedChannelCheck.enabled = enabled; self.filterEventUserLeftChannelCheck.enabled = enabled; self.filterEventUserKickedFromChannelCheck.enabled = enabled; self.filterEventUserDisconnectedCheck.enabled = enabled; self.filterEventUserChangedNicknameCheck.enabled = enabled; self.filterEventChannelTopicReceivedCheck.enabled = enabled; self.filterEventChannelTopicChangedCheck.enabled = enabled; self.filterEventChannelModeReceivedCheck.enabled = enabled; self.filterEventChannelModeChangedCheck.enabled = enabled; } - (void)updateEnabledStateOfComponentsConstrainedByFilterEvents { BOOL enabled = (self.filterEventPlainTextMessageCheck.state == NSControlStateValueOn || self.filterEventActionMessageCheck.state == NSControlStateValueOn || self.filterEventNoticeMessageCheck.state == NSControlStateValueOn); self.filterIgnoreOperatorsCheckEnabled = enabled; [self willChangeValueForKey:@"filterIgnoreOperatorsCheckValue"]; [self didChangeValueForKey:@"filterIgnoreOperatorsCheckValue"]; } - (void)updateEnabledStateOfSenderMatch { BOOL enabled = (self.filterLimitedToMyselfCheck.state == NSControlStateValueOff); self.filterSenderMatchTextField.enabled = enabled; } - (void)updateFilterLimitedToMatrix { TPI_ChatFilterLimitToValue limitedTo = self.filter.filterLimitedToValue; self.filterLimitToNoLimitButton.state = (limitedTo == TPI_ChatFilterLimitToValueNoLimit); self.filterLimitToOnlyChannelsButton.state = (limitedTo == TPI_ChatFilterLimitToValueChannels); self.filterLimitToOnlyPrivateMessagesButton.state = (limitedTo == TPI_ChatFilterLimitToValuePrivateMessages); self.filterLimitToSpecificItemsButton.state = (limitedTo == TPI_ChatFilterLimitToValueSpecificItems); } - (void)filterLimitedToMyselfChanged:(id)sender { [self updateEnabledStateOfSenderMatch]; } - (void)filterEventTypeChanged:(id)sender { [self updateEnabledStateOfComponentsConstrainedByFilterEvents]; } - (void)viewFilterMatchHelpText:(id)sender { [TLOpenLink openWithString:@"https://help.codeux.com/textual/Introduction-to-the-Chat-Filter-Addon.kb#faq-entry-1" inBackground:NO]; } - (void)viewFilterActionHelpText:(id)sender { [TLOpenLink openWithString:@"https://help.codeux.com/textual/Introduction-to-the-Chat-Filter-Addon.kb#faq-entry-2" inBackground:NO]; } - (void)viewFilterSenderMatchHelpText:(id)sender { [TLOpenLink openWithString:@"https://help.codeux.com/textual/Introduction-to-the-Chat-Filter-Addon.kb#faq-entry-3" inBackground:NO]; } - (void)viewFilterForwardToDestinationHelpText:(id)sender { [TLOpenLink openWithString:@"https://help.codeux.com/textual/Introduction-to-the-Chat-Filter-Addon.kb#faq-entry-4" inBackground:NO]; } - (void)filterLimitedToMatrixChanged:(id)sender { self.filter.filterLimitedToValue = [sender tag]; [self updateVisibilityOfLimitedToTableHostView]; [self updateEnabledStateOfFilterEvents]; } - (void)filterIgnoreContentCheckChanged:(id)sender { [self updateEnableStateOfFilterActionTokenField]; [self toggleOkButton]; } @end #pragma mark - #pragma mark Token Object @implementation TPI_ChatFilterFilterActionToken + (TPI_ChatFilterFilterActionToken *)tokenWithToken:(NSString *)token { TPI_ChatFilterFilterActionToken *tokenField = [TPI_ChatFilterFilterActionToken new]; tokenField.token = token; return tokenField; } + (nullable TPI_ChatFilterFilterActionToken *)tokenWithTokenTitle:(NSString *)tokenTitle { NSArray *tokenTitles = [TPI_ChatFilterFilterActionToken tokenTitles]; NSInteger tokenTitleIndex = [tokenTitles indexOfObject:tokenTitle]; if (tokenTitleIndex == NSNotFound) { return nil; } NSArray *tokens = [TPI_ChatFilterFilterActionToken tokens]; NSString *token = tokens[tokenTitleIndex]; return [TPI_ChatFilterFilterActionToken tokenWithToken:token]; } + (BOOL)isToken:(NSString *)token { NSArray *tokens = [TPI_ChatFilterFilterActionToken tokens]; return ([tokens indexOfObject:token] != NSNotFound); } + (NSArray *)tokens { /* The index of this array should match the index of -tokenTitles */ static NSArray *tokens = nil; if (tokens == nil) { tokens = @[ @"%_channelName_%", @"%_localNickname_%", @"%_networkName_%", @"%_originalMessage_%", @"%_senderNickname_%", @"%_senderUsername_%", @"%_senderAddress_%", @"%_senderHostmask_%", @"%_serverAddress_%", @"%_Parameter_0_%", @"%_Parameter_1_%", @"%_Parameter_2_%", @"%_Parameter_3_%", @"%_Parameter_4_%", @"%_Parameter_5_%", @"%_Parameter_6_%", @"%_Parameter_7_%", @"%_Parameter_8_%" ]; } return tokens; } + (NSArray *)tokenTitles { /* The index of this array should match the index of -tokens */ static NSArray *tokens = nil; if (tokens == nil) { tokens = @[ TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[90e-tj]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[tbc-wc]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[840-f9]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[sch-hi]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[k82-6i]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[2sk-ui]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[xt2-bv]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[je5-u2]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[9xy-vf]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[kph-dc]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[8bd-nt]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[4vk-v8]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[kvz-ej]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[2pa-ju]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[jen-7o]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[t5v-4o]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[vyf-el]"), TPILocalizedString(@"TPI_ChatFilterEditFilterSheet[0x4-ib]") ]; } return tokens; } + (nullable NSString *)titleForToken:(NSString *)token { NSArray *tokens = [TPI_ChatFilterFilterActionToken tokens]; NSInteger tokenIndex = [tokens indexOfObject:token]; if (tokenIndex == NSNotFound) { return nil; } NSArray *tokenTitles = [TPI_ChatFilterFilterActionToken tokenTitles]; return tokenTitles[tokenIndex]; } - (nullable NSString *)tokenTitle { return [TPI_ChatFilterFilterActionToken titleForToken:self.token]; } - (NSString *)description { return self.token; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilterExtension.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_ChatFilterExtension : NSObject @property (readonly) NSArrayController *filterArrayController; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilterExtension.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPI_ChatFilterExtension.h" #import "TPI_ChatFilterEditFilterSheet.h" #import "TPI_ChatFilterLogic.h" #import "THOPluginProtocolPrivate.h" NS_ASSUME_NONNULL_BEGIN #define _filterTableDragToken @"filterTableDragToken" #define _filterListUserDefaultsKey @"Textual Chat Filter Extension -> Filters" @interface TPI_ChatFilterExtension () @property (nonatomic, strong) IBOutlet NSView *preferencesPaneView; @property (nonatomic, strong) IBOutlet NSMenu *filterAddMenu; @property (nonatomic, weak) IBOutlet NSButton *filterAddButton; @property (nonatomic, weak) IBOutlet NSButton *filterRemoveButton; @property (nonatomic, weak) IBOutlet NSButton *filterEditButton; @property (nonatomic, weak) IBOutlet TVCBasicTableView *filterTable; @property (nonatomic, strong, readwrite) IBOutlet NSArrayController *filterArrayController; @property (nonatomic, assign) BOOL atleastOneFilterExists; @property (nonatomic, assign) NSInteger activeChatFilterIndex; @property (nonatomic, strong) TPI_ChatFilterEditFilterSheet *activeChatFilterEditSheet; @property (nonatomic, strong) TPI_ChatFilterLogic *filterLogicController; @property (nonatomic, assign) BOOL savingFilters; - (IBAction)filterTableDoubleClicked:(id)sender; - (IBAction)presentFilterAddMenu:(id)sender; - (IBAction)filterAdd:(id)sender; - (IBAction)filterRemove:(id)sender; - (IBAction)filterEdit:(id)sender; - (IBAction)filterDuplicate:(id)sender; - (IBAction)filterExport:(id)sender; - (IBAction)filterImport:(id)sender; @end @implementation TPI_ChatFilterExtension #pragma mark - #pragma mark Filter Logic - (BOOL)receivedCommand:(NSString *)command withText:(nullable NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt referenceMessage:(nullable IRCMessage *)referenceMessage { return [self.filterLogicController receivedCommand:command withText:text authoredBy:textAuthor destinedFor:textDestination onClient:client receivedAt:receivedAt referenceMessage:referenceMessage]; } - (BOOL)receivedText:(NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination asLineType:(TVCLogLineType)lineType onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt wasEncrypted:(BOOL)wasEncrypted { return [self.filterLogicController receivedText:text authoredBy:textAuthor destinedFor:textDestination asLineType:lineType onClient:client receivedAt:receivedAt wasEncrypted:wasEncrypted]; } #pragma mark - #pragma mark Internal Filter List Storage - (void)reloadFilters { [self.filterArrayController removeAllArrangedObjects]; [self loadFilters]; [self.filterLogicController reloadFilterActionPerforms]; } - (void)loadFilters { NSArray *filterConfigurations = [RZUserDefaults() arrayForKey:_filterListUserDefaultsKey]; for (id filterConfiguration in filterConfigurations) { if ([filterConfiguration isKindOfClass:[NSDictionary class]] == NO) { continue; } TPI_ChatFilter *filter = [[TPI_ChatFilter alloc] initWithDictionary:filterConfiguration]; [self.filterArrayController addObject:filter]; } [self reloadFilterCount]; } - (void)saveFilters { self.savingFilters = YES; NSArray *filters = self.filterArrayController.arrangedObjects; NSMutableArray *filterConfigurations = [NSMutableArray arrayWithCapacity:filters.count]; for (TPI_ChatFilter *filter in filters) { [filterConfigurations addObject:filter.dictionaryValue]; } [RZUserDefaults() setObject:[filterConfigurations copy] forKey:_filterListUserDefaultsKey]; [self reloadFilterCount]; } - (void)reloadFilterCount { NSArray *arrangedObjects = self.filterArrayController.arrangedObjects; self.atleastOneFilterExists = (arrangedObjects.count > 0); } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context { if ([keyPath isEqualToString:_filterListUserDefaultsKey]) { if (self.savingFilters) { self.savingFilters = NO; return; } [self reloadFilters]; } } #pragma mark - #pragma mark Preference Pane - (void)pluginLoadedIntoMemory { XRPerformBlockSynchronouslyOnMainQueue(^{ NSAssert([TPIBundleFromClass() loadNibNamed:@"TPI_ChatFilterExtension" owner:self topLevelObjects:nil], @"Failed to load user interface"); }); self.activeChatFilterIndex = (-1); self.atleastOneFilterExists = NO; self.filterLogicController = [[TPI_ChatFilterLogic alloc] initWithParentObject:self]; [self loadFilters]; [RZUserDefaults() addObserver:self forKeyPath:_filterListUserDefaultsKey options:NSKeyValueObservingOptionNew context:NULL]; } - (void)pluginWillBeUnloadedFromMemory { self.filterLogicController = nil; [RZUserDefaults() removeObserver:self forKeyPath:_filterListUserDefaultsKey]; } - (NSView *)pluginPreferencesPaneView { return self.preferencesPaneView; } - (NSString *)pluginPreferencesPaneMenuItemName { return TPILocalizedString(@"TPI_ChatFilterExtension[jq1-6r]"); } - (void)awakeFromNib { [self.filterTable registerForDraggedTypes:@[_filterTableDragToken]]; } - (void)filterTableDoubleClicked:(id)sender { [self filterEdit:sender]; } - (void)filterAdd:(id)sender { [self editFilter:nil]; } - (void)filterRemove:(id)sender { BOOL performRemove = [TDCAlert modalAlertWithMessage:TPILocalizedString(@"TPI_ChatFilterExtension[dj6-fn]") title:TPILocalizedString(@"TPI_ChatFilterExtension[c0k-xj]") defaultButton:TPILocalizedString(@"TPI_ChatFilterExtension[jvu-m7]") alternateButton:TPILocalizedString(@"TPI_ChatFilterExtension[p5s-ff]")]; if (performRemove == NO) { return; } NSInteger selectedRow = self.filterTable.selectedRow; if (selectedRow < 0) { return; } [self.filterArrayController removeObjectAtArrangedObjectIndex:selectedRow]; [self saveFilters]; } - (void)filterEdit:(id)sender { NSInteger selectedRow = self.filterTable.selectedRow; if (selectedRow < 0) { return; } TPI_ChatFilter *filter = self.filterArrayController.arrangedObjects[selectedRow]; [self editFilter:filter atIndex:selectedRow]; } - (void)editFilter:(id)filter { [self editFilter:filter atIndex:(-1)]; } - (void)editFilter:(id)filter atIndex:(NSInteger)filterIndex { self.activeChatFilterIndex = filterIndex; TPI_ChatFilterEditFilterSheet *sheet = [[TPI_ChatFilterEditFilterSheet alloc] initWithFilter:filter]; sheet.delegate = self; sheet.window = [NSApp keyWindow]; [sheet start]; self.activeChatFilterEditSheet = sheet; } - (void)chatFilterEditFilterSheet:(TPI_ChatFilterEditFilterSheet *)sender onOk:(TPI_ChatFilter *)filter { if ([filter isKindOfClass:[TPI_ChatFilterMutable class]]) { filter = [filter copy]; } if (self.activeChatFilterIndex < 0) { [self.filterArrayController addObject:filter]; } else { [self.filterArrayController replaceObjectAtArrangedObjectIndex:self.activeChatFilterIndex withObject:filter]; } [self saveFilters]; [self.filterLogicController reloadFilterActionPerforms]; } - (void)chatFilterEditFilterSheetWillClose:(TPI_ChatFilterEditFilterSheet *)sender { self.activeChatFilterIndex = (-1); self.activeChatFilterEditSheet = nil; } - (void)filterDuplicate:(id)sender { NSInteger selectedRow = self.filterTable.selectedRow; TPI_ChatFilter *filter = self.filterArrayController.arrangedObjects[selectedRow]; TPI_ChatFilterMutable *filterNew = [filter mutableCopy]; filterNew.filterTitle = [filterNew.filterTitle stringByAppendingString:@" (Duplicate)"]; [self editFilter:[filterNew copy] atIndex:(-1)]; } - (void)filterExport:(id)sender { NSInteger selectedRow = self.filterTable.selectedRow; TPI_ChatFilter *filter = self.filterArrayController.arrangedObjects[selectedRow]; NSSavePanel *saveDialog = [NSSavePanel savePanel]; saveDialog.canCreateDirectories = YES; saveDialog.nameFieldStringValue = TXLocalizationNotNeeded(@"filter.plist"); [saveDialog beginSheetModalForWindow:[NSApp keyWindow] completionHandler:^(NSInteger returnCode) { if (returnCode != NSModalResponseOK) { return; } NSURL *pathURL = saveDialog.URL; [filter writeToURL:pathURL]; }]; } - (void)filterImport:(id)sender { NSOpenPanel *openDialog = [NSOpenPanel openPanel]; openDialog.allowsMultipleSelection = NO; openDialog.canChooseDirectories = NO; openDialog.canChooseFiles = YES; openDialog.canCreateDirectories = NO; openDialog.resolvesAliases = YES; openDialog.message = TPILocalizedString(@"TPI_ChatFilterExtension[i9c-s3]"); openDialog.prompt = TPILocalizedString(@"TPI_ChatFilterExtension[2tc-m7]"); [openDialog beginSheetModalForWindow:[NSApp keyWindow] completionHandler:^(NSInteger returnCode) { if (returnCode != NSModalResponseOK) { return; } [openDialog orderOut:nil]; NSURL *pathURL = openDialog.URL; TPI_ChatFilter *filter = [[TPI_ChatFilter alloc] initWithContentsOfURL:pathURL]; if (filter == nil) { [TDCAlert modalAlertWithMessage:@"" title:TPILocalizedString(@"TPI_ChatFilterExtension[eqr-7t]") defaultButton:TPILocalizedString(@"TPI_ChatFilterExtension[ybz-7i]") alternateButton:nil]; return; } [self editFilter:filter]; }]; } - (void)presentFilterAddMenu:(id)sender { [self.filterAddMenu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0, 0) inView:sender]; } #pragma mark - #pragma mark Table View Delegate - (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pasteboard { NSData *draggedData = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes]; [pasteboard declareTypes:@[_filterTableDragToken] owner:self]; [pasteboard setData:draggedData forType:_filterTableDragToken]; return YES; } - (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation { return NSDragOperationGeneric; } - (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id )info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation { NSPasteboard *pasteboard = [info draggingPasteboard]; NSData *draggedData = [pasteboard dataForType:_filterTableDragToken]; NSIndexSet *draggedRowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:draggedData]; NSUInteger draggedRowIndex = draggedRowIndexes.firstIndex; [self.filterArrayController moveObjectAtArrangedObjectIndex:draggedRowIndex toIndex:row]; [self saveFilters]; return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilterInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPI_ChatFilter.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_ChatFilter () { @protected BOOL _filterIgnoreContent; BOOL _filterIgnoreOperators; BOOL _filterLogMatch; BOOL _filterLimitedToMyself; TPI_ChatFilterEventType _filterEvents; TPI_ChatFilterLimitToValue _filterLimitedToValue; TPI_ChatFilterAgeComparator _filterAgeComparator; NSUInteger _filterAgeLimit; NSUInteger _filterActionFloodControlInterval; NSArray *_filterLimitedToChannelsIDs; NSArray *_filterLimitedToClientsIDs; NSArray *_filterEventsNumerics; NSString *_filterAction; NSString *_filterForwardToDestination; NSString *_filterMatch; NSString *_filterNotes; NSString *_filterSenderMatch; NSString *_filterTitle; @private NSString *_uniqueIdentifier; NSDictionary *_defaults; NSCache *_cachedIsCommandEnabledResponses; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilterLogic.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" #import "TPI_ChatFilter.h" #import "TPI_ChatFilterExtension.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_ChatFilterLogic : NSObject - (instancetype)initWithParentObject:(TPI_ChatFilterExtension *)parentObject; - (void)reloadFilterActionPerforms; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_ChatFilterLogic.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPI_ChatFilterLogic.h" #import "IRCClientPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_ChatFilterLogic () @property (nonatomic, weak) TPI_ChatFilterExtension *parentObject; @property (nonatomic, strong) NSMutableDictionary *filterActionLastPerforms; @end @implementation TPI_ChatFilterLogic - (instancetype)initWithParentObject:(TPI_ChatFilterExtension *)parentObject { NSParameterAssert(parentObject != nil); if ((self = [super init])) { self.parentObject = parentObject; [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.filterActionLastPerforms = [NSMutableDictionary dictionary]; } - (BOOL)testFilterDestination:(TPI_ChatFilter *)filter authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination onClient:(IRCClient *)client { /* Try to resolve destination channel now that we know that there is a filter that will need it. */ TPI_ChatFilterLimitToValue filterLimitedToValue = filter.filterLimitedToValue; if (filterLimitedToValue != TPI_ChatFilterLimitToValueNoLimit || filter.filterIgnoreOperators) { if (textDestination == nil && textAuthor.isServer == NO) { LogToConsoleDebugWithSubsystem(THOPluginLoggingSubsystem(), "textDestination == nil — Returning input instead of continuing with filter"); return NO; } } if (filterLimitedToValue == TPI_ChatFilterLimitToValueChannels) { if (textDestination.isChannel == NO) { /* Filter is limited to a channel but the destination is not a channel. */ return NO; } } else if (filterLimitedToValue == TPI_ChatFilterLimitToValuePrivateMessages) { if (textDestination.isPrivateMessage == NO) { /* Filter is limited to a private message but the destination is not a private message. */ return NO; } } else if (filterLimitedToValue == TPI_ChatFilterLimitToValueSpecificItems) { NSArray *filterLimitedToClientsIDs = filter.filterLimitedToClientsIDs; NSArray *filterLimitedToChannelsIDs = filter.filterLimitedToChannelsIDs; if ([filterLimitedToClientsIDs containsObject:client.uniqueIdentifier] == NO && [filterLimitedToChannelsIDs containsObject:textDestination.uniqueIdentifier] == NO) { /* Target channel is not covered by current filter. */ return NO; } } return YES; } - (BOOL)testFilterSender:(TPI_ChatFilter *)filter authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination onClient:(IRCClient *)client { /* Check whether the sender is myself */ if (filter.filterLimitedToMyself) { NSString *comparisonValue1 = client.userNickname; NSString *comparisonValue2 = textAuthor.nickname; if ([comparisonValue1 isEqualToStringIgnoringCase:comparisonValue2] == NO) { return NO; } return YES; } /* Maybe perform filter action on sender hostmask */ NSString *filterSenderMatch = filter.filterSenderMatch; if (filterSenderMatch.length > 0) { NSString *comparisonHostmask = nil; if (textAuthor.isServer) { comparisonHostmask = textAuthor.nickname; // Server address } else { comparisonHostmask = textAuthor.hostmask; } if ([XRRegularExpression string:comparisonHostmask isMatchedByRegex:filterSenderMatch withoutCase:YES] == NO) { /* If a filter specifies a sender match and the match for this particular filter fails, then skip this filter. */ return NO; } } /* For the next few checks we can ignore them if destination is not a channel. */ if (textDestination.isChannel == NO || textAuthor.isServer) { return YES; } IRCChannelUser *senderUser = [textDestination findMember:textAuthor.nickname]; /* Check age of sender */ NSInteger filterAgeLimit = filter.filterAgeLimit; if (filterAgeLimit > 0) { /* The value of senderUser is checked here and not where it is declared so that filters that do not rely on the value of this object are passed over. */ if (senderUser == nil) { return NO; } NSInteger ageLimitDelta = [NSDate timeIntervalSinceNow:senderUser.creationTime]; switch (filter.filterAgeComparator) { case TPI_ChatFilterAgeComparatorLessThan: { if (ageLimitDelta < filterAgeLimit) { return NO; // ignore this filter } break; } case TPI_ChatFilterAgeComparatorGreaterThan: { if (ageLimitDelta >= filterAgeLimit) { return NO; // ignore this filter } break; } default: { break; } } // switch() } /* Is sender an operator? */ if (filter.filterIgnoreOperators) { if (senderUser == nil) { return NO; } if (senderUser.halfOp) { /* User is at least a Half-op, ignore this filter. */ return NO; } } return YES; } - (BOOL)testFilterMatch:(TPI_ChatFilter *)filter againstText:(nullable NSString *)text allowingNilText:(BOOL)allowingNilText { /* Filter text */ if (text == nil) { if (allowingNilText) { return YES; } else { return NO; } } NSString *filterMatch = filter.filterMatch; if (filterMatch.length > 0) { if ([TPCPreferences removeAllFormatting] == NO) { text = text.stripIRCEffects; } if ([XRRegularExpression string:text isMatchedByRegex:filterMatch withoutCase:YES] == NO) { /* The input text is not matched by the filter match. Continue to the next filter to try again. */ return NO; } } return YES; } - (BOOL)receivedCommand:(NSString *)command withText:(nullable NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt referenceMessage:(nullable IRCMessage *)referenceMessage { /* Begin processing filters */ NSArray *filters = self.parentObject.filterArrayController.content; for (TPI_ChatFilter *filter in filters) { @autoreleasepool { if ([filter isCommandEnabled:command] == NO) { /* Continue to next filter. This filter is not interested in the line type of the input. */ continue; } /* Perform common filter checks */ if ([self testFilterDestination:filter authoredBy:textAuthor destinedFor:textDestination onClient:client] == NO) { continue; } if ([self testFilterSender:filter authoredBy:textAuthor destinedFor:textDestination onClient:client] == NO) { continue; } if ([self testFilterMatch:filter againstText:text allowingNilText:YES] == NO) { continue; } /* Perform actions defined by filter */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [self performActionForFilter:filter withOriginalMessage:text authoredBy:textAuthor destinedFor:textDestination onClient:client referenceMessage:referenceMessage]; }); /* Forward a copy of the message to a query? */ NSString *filterForwardToDestination = filter.filterForwardToDestination; if (filterForwardToDestination.length > 0 && text.length > 0) { IRCChannel *destinationChannel = [client findChannelOrCreate:filterForwardToDestination isPrivateMessage:YES]; NSString *message = TPILocalizedString(@"TPI_ChatFilterLogic[dct-7h]", command, text); [client print:message by:nil inChannel:destinationChannel asType:TVCLogLineTypeDebug command:TVCLogLineDefaultCommandValue receivedAt:receivedAt isEncrypted:NO referenceMessage:nil completionBlock:^(TVCLogControllerPrintOperationContext *context) { [client setUnreadStateForChannel:destinationChannel]; }]; } if (filter.filterIgnoreContent) { return NO; // Ignore original content } /* Return once the first filter matches */ return YES; } // @autorelease } // for return YES; } - (BOOL)receivedText:(NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(nullable IRCChannel *)textDestination asLineType:(TVCLogLineType)lineType onClient:(IRCClient *)client receivedAt:(NSDate *)receivedAt wasEncrypted:(BOOL)wasEncrypted { /* Begin processing filters */ NSArray *filters = self.parentObject.filterArrayController.content; for (TPI_ChatFilter *filter in filters) { @autoreleasepool { if (((lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypePrivateMessageNoHighlight) && [filter isEventTypeEnabled:TPI_ChatFilterEventTypePlainTextMessage] == NO) || ((lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeActionNoHighlight) && [filter isEventTypeEnabled:TPI_ChatFilterEventTypeActionMessage] == NO) || (lineType == TVCLogLineTypeNotice && [filter isEventTypeEnabled:TPI_ChatFilterEventTypeNoticeMessage] == NO)) { /* Continue to next filter. This filter is not interested in the line type of the input. */ continue; } /* Perform common filter checks */ if ([self testFilterDestination:filter authoredBy:textAuthor destinedFor:textDestination onClient:client] == NO) { continue; } if ([self testFilterSender:filter authoredBy:textAuthor destinedFor:textDestination onClient:client] == NO) { continue; } if ([self testFilterMatch:filter againstText:text allowingNilText:NO] == NO) { continue; } /* Perform actions defined by filter */ XRPerformBlockAsynchronouslyOnMainQueue(^{ [self performActionForFilter:filter withOriginalMessage:text authoredBy:textAuthor destinedFor:textDestination onClient:client referenceMessage:nil]; }); /* Forward a copy of the message to a query? */ NSString *filterForwardToDestination = filter.filterForwardToDestination; if (filterForwardToDestination.length > 0 && text.length > 0) { IRCChannel *destinationChannel = [client findChannelOrCreate:filterForwardToDestination isPrivateMessage:YES]; NSString *fakeMessageCommand = nil; if (lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypePrivateMessageNoHighlight || lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypeActionNoHighlight) { fakeMessageCommand = @"PRIVMSG"; } else if (lineType == TVCLogLineTypeNotice) { fakeMessageCommand = @"NOTICE"; } [client print:text by:textAuthor.nickname inChannel:destinationChannel asType:lineType command:fakeMessageCommand receivedAt:receivedAt isEncrypted:wasEncrypted referenceMessage:nil completionBlock:^(TVCLogControllerPrintOperationContext *context) { if (lineType == TVCLogLineTypeNotice) { [client setUnreadStateForChannel:destinationChannel]; } else { BOOL isHighlight = context.highlight; if (isHighlight) { [client setHighlightStateForChannel:destinationChannel]; } [client setUnreadStateForChannel:destinationChannel isHighlight:isHighlight]; } }]; } if (filter.filterIgnoreContent) { return NO; // Ignore original content } /* Return once the first filter matches */ return YES; } // @autorelease } // for return YES; } - (void)performActionForFilter:(TPI_ChatFilter *)filter withOriginalMessage:(nullable NSString *)text authoredBy:(IRCPrefix *)textAuthor destinedFor:(IRCChannel *)textDestination onClient:(IRCClient *)client referenceMessage:(nullable IRCMessage *)referenceMessage { if ([self isItSafeToPerformActionForFilter:filter] == NO) { return; } if (text == nil) { text = @""; } /* Perform action */ NSString *filterAction = filter.filterAction; if (filterAction.length == 0) { return; } #define _maybeReplaceValue(key, value) \ if (value == nil) { \ filterAction = [filterAction stringByReplacingOccurrencesOfString:(key) withString:@""]; \ } else { \ filterAction = [filterAction stringByReplacingOccurrencesOfString:(key) withString:(value)]; \ } #define _maybeReplaceParam(paramIndex, paramIndexString) \ if (paramIndex >= paramsCount) { \ filterAction = [filterAction stringByReplacingOccurrencesOfString:@paramIndexString withString:@""]; \ } else { \ filterAction = [filterAction stringByReplacingOccurrencesOfString:@paramIndexString withString:params[paramIndex]]; \ } _maybeReplaceValue(@"%_channelName_%", textDestination.name) _maybeReplaceValue(@"%_localNickname_%", client.userNickname) _maybeReplaceValue(@"%_networkName_%", client.networkName) _maybeReplaceValue(@"%_originalMessage_%", text) _maybeReplaceValue(@"%_senderNickname_%", textAuthor.nickname) _maybeReplaceValue(@"%_senderUsername_%", textAuthor.username) _maybeReplaceValue(@"%_senderAddress_%", textAuthor.address) _maybeReplaceValue(@"%_senderHostmask_%", textAuthor.hostmask) _maybeReplaceValue(@"%_serverAddress_%", client.serverAddress) NSArray *params = referenceMessage.params; NSUInteger paramsCount = params.count; _maybeReplaceParam(0, "%_Parameter_0_%") _maybeReplaceParam(1, "%_Parameter_1_%") _maybeReplaceParam(2, "%_Parameter_2_%") _maybeReplaceParam(3, "%_Parameter_3_%") _maybeReplaceParam(4, "%_Parameter_4_%") _maybeReplaceParam(5, "%_Parameter_5_%") _maybeReplaceParam(6, "%_Parameter_6_%") _maybeReplaceParam(7, "%_Parameter_7_%") _maybeReplaceParam(8, "%_Parameter_8_%") _maybeReplaceParam(9, "%_Parameter_9_%") #undef _maybeReplaceParam #undef _maybeReplaceValue NSArray *filterActions = [filterAction componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; for (__strong NSString *actionCommand in filterActions) { if (actionCommand.length > 1 && [actionCommand hasPrefix:@"/"] && [actionCommand hasPrefix:@"//"] == NO) { actionCommand = [actionCommand substringFromIndex:1]; [client sendCommand:actionCommand]; } } /* Log action to a private message */ if (filter.filterLogMatch) { IRCChannel *filterActionReportQuery = [client findChannelOrCreate:@"Filter Actions" isUtility:YES]; NSString *formattedMessage = nil; if (textDestination == nil) { formattedMessage = TPILocalizedString(@"TPI_ChatFilterExtension[yla-he]", filter.filterTitle, textAuthor.nickname); } else { formattedMessage = TPILocalizedString(@"TPI_ChatFilterExtension[jcm-xj]", filter.filterTitle, textAuthor.nickname, textDestination.name); } [client print:formattedMessage by:nil inChannel:filterActionReportQuery asType:TVCLogLineTypePrivateMessage command:@"PRIVMSG"]; [client setUnreadStateForChannel:filterActionReportQuery]; } } - (BOOL)isItSafeToPerformActionForFilter:(TPI_ChatFilter *)filter { NSUInteger floodControlInterval = filter.filterActionFloodControlInterval; if (floodControlInterval == 0) { return YES; } NSTimeInterval now = [NSDate timeIntervalSince1970]; @synchronized (self.filterActionLastPerforms) { NSString *filterIdentifier = filter.uniqueIdentifier; NSTimeInterval filterLastPerform = [self.filterActionLastPerforms doubleForKey:filterIdentifier]; if ((now - filterLastPerform) <= floodControlInterval) { LogToConsoleDebugWithSubsystem(THOPluginLoggingSubsystem(), "Not performing action because of flood control: %{public}.2f %{public}.2f", now, filterLastPerform); return NO; } self.filterActionLastPerforms[filterIdentifier] = @(now); } return YES; } - (void)reloadFilterActionPerforms { /* This rebuilds the -filterActionLastPerforms so that the only entries that exist are 1) filters that still exist 2) filters that require a timer */ @synchronized (self.filterActionLastPerforms) { NSMutableDictionary *filterLastPerformsOld = self.filterActionLastPerforms; NSMutableDictionary *filterLastPerformsNew = [NSMutableDictionary dictionary]; NSArray *filters = self.parentObject.filterArrayController.content; for (TPI_ChatFilter *filter in filters) { @autoreleasepool { if (filter.filterActionFloodControlInterval == 0) { continue; } NSString *filterIdentifier = filter.uniqueIdentifier; NSNumber *filterLastPerform = filterLastPerformsOld[filterIdentifier]; if (filterLastPerform == nil) { continue; } filterLastPerformsNew[filterIdentifier] = filterLastPerform; } } self.filterActionLastPerforms = filterLastPerformsNew; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_NumberOnlyTextFieldFormatter.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_NumberOnlyTextFieldFormatter : NSNumberFormatter @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Classes/TPI_NumberOnlyTextFieldFormatter.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPI_NumberOnlyTextFieldFormatter.h" NS_ASSUME_NONNULL_BEGIN @implementation TPI_NumberOnlyTextFieldFormatter - (BOOL)isPartialStringValid:(NSString *)partialString newEditingString:(NSString **)newString errorDescription:(NSString **)error { if (partialString.length == 0) { return YES; } if (partialString.numericOnly == NO) { NSBeep(); return NO; } return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Chat Filter/Resources/Language Files/en.lproj/TPI_ChatFilterEditFilterSheet.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Next unused key: 1014 */ "90e-tj" = "Channel Name"; "tbc-wc" = "My Nickname"; "840-f9" = "Network Name"; "sch-hi" = "Original Message"; "xt2-bv" = "Sender Address"; "je5-u2" = "Sender Hostmask"; "k82-6i" = "Sender Nickname"; "2sk-ui" = "Sender Username"; "9xy-vf" = "Server Address"; "kph-dc" = "Parameter #1"; "8bd-nt" = "Parameter #2"; "4vk-v8" = "Parameter #3"; "kvz-ej" = "Parameter #4"; "2pa-ju" = "Parameter #5"; "jen-7o" = "Parameter #6"; "t5v-4o" = "Parameter #7"; "vyf-el" = "Parameter #8"; "0x4-ib" = "Parameter #9"; "m0u-tw" = "Please enter a shorter destination"; "5kd-jt" = "Please enter a properly formatted destination. Accepted characters: a-z, A-Z, 0-9, dashes (-), underscores (_), and spaces."; "9ri-sd" = "Please enter a properly formatted list of commands. • Commands must be a comma (,) separated list.\n• Commands must only contain the characters: a-z, A-Z, 0-9 • Numerics must be whole numbers that are greater than zero (0) and less than or equal to 999."; ================================================ FILE: Sources/Plugins/Chat Filter/Resources/Language Files/en.lproj/TPI_ChatFilterExtension.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "jq1-6r" = "Chat Filter"; "yla-he" = "Performing action defined by the filter “%1$@“ against the user “%2$@“"; "jcm-xj" = "Performing action defined by the filter “%1$@“ against the user “%2$@“ in the channel “%3$@“"; "dka-bx" = "Filter “%@“"; "c0k-xj" = "Are you sure that you want to delete this filter?"; "dj6-fn" = "There is no undo and all data related to this filter will be erased."; "jvu-m7" = "Yes"; "p5s-ff" = "No"; "i9c-s3" = "Select filter configuration file to import"; "2tc-m7" = "Select"; "eqr-7t" = "Configuration file is in format that cannot be read"; "ybz-7i" = "OK"; ================================================ FILE: Sources/Plugins/Chat Filter/Resources/Language Files/en.lproj/TPI_ChatFilterLogic.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "dct-7h" = "Message(%1$@): %2$@"; ================================================ FILE: Sources/Plugins/Chat Filter/Resources/Property Lists/Info.plist ================================================ CFBundleDevelopmentRegion English CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.0 NSHumanReadableCopyright Copyright © 2015 - 2020 Codeux Software, LLC. All rights reserved. MinimumTextualVersion 7.2.4 NSPrincipalClass TPI_ChatFilterExtension ================================================ FILE: Sources/Plugins/Chat Filter/Resources/User Interface/en.lproj/TPI_ChatFilterEditFilterSheet.xib ================================================ Select the event(s) that this filter will respond to. Some events are matched based on sender information and do not contain text. See documentation for examples. Not all events can be narrowed to only you. Filters act on incoming data. You wont be able to use this option to self-bot. The most common event this option is used for is Channel Join. Use the box presented below to keep notes specific to this filter such as examples of messages that will trigger it. This information is for your records and does not alter the behavior of the filter. ================================================ FILE: Sources/Plugins/Chat Filter/Resources/User Interface/en.lproj/TPI_ChatFilterExtension.xib ================================================ A “filter” is a powerful, customizable rule that allows you to hide messages or perform commands (including external scripts). They are triggered by specific text appearing in a message or events occurring such as join, part, quit, etc. Click the + button in the bottom left corner to add your first filter. Drag and drop filters to change their priority. The table is ranked highest priority to lowest. Only one filter can match a specific message or event. filterDescription ================================================ FILE: Sources/Plugins/Sample Code/Preference Pane/Classes/TPI_PreferencePaneExample.h ================================================ #import "Textual.h" @interface TPI_PreferencePaneExample : NSObject @property (nonatomic, strong) IBOutlet NSView *ourView; - (IBAction)preferenceChanged:(id)sender; @end ================================================ FILE: Sources/Plugins/Sample Code/Preference Pane/Classes/TPI_PreferencePaneExample.m ================================================ #import "TPI_PreferencePaneExample.h" @implementation TPI_PreferencePaneExample - (NSView *)pluginPreferencesPaneView { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ if ([[NSBundle bundleForClass:self.class] loadNibNamed:@"PreferencePane" owner:self topLevelObjects:nil] == NO) { NSAssert(NO, @"TPI_PreferencePaneExample: Failed to load view"); } }); return self.ourView; } - (NSString *)pluginPreferencesPaneMenuItemName { return @"My Test Plugin"; } - (void)doSomethingWithPreferences { BOOL isSomethingChecked = [[NSUserDefaults standardUserDefaults] boolForKey:@"TPI_PreferencesSomethingCheckboxIsChecked"]; if (isSomethingChecked) { NSLog(@"Checkbox is checked"); } else { NSLog(@"Checkbox is not checked"); } } - (IBAction)preferenceChanged:(id)sender { [self doSomethingWithPreferences]; } @end ================================================ FILE: Sources/Plugins/Sample Code/Preference Pane/PreferencePaneExample.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 4C0BA81020ECB0CB00C9D351 /* PreferencePane.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C0BA80D20ECB0CB00C9D351 /* PreferencePane.xib */; }; 4C51BE9412D0471600E79CEB /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C51BE9312D0471600E79CEB /* Cocoa.framework */; }; 4CF30A1F20EC73480028D64F /* TPI_PreferencePaneExample.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CF30A1B20EC73480028D64F /* TPI_PreferencePaneExample.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C0BA80D20ECB0CB00C9D351 /* PreferencePane.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PreferencePane.xib; sourceTree = ""; }; 4C0BA80F20ECB0CB00C9D351 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C51BE9312D0471600E79CEB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 4CF30A1A20EC73480028D64F /* TPI_PreferencePaneExample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_PreferencePaneExample.h; sourceTree = ""; }; 4CF30A1B20EC73480028D64F /* TPI_PreferencePaneExample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_PreferencePaneExample.m; sourceTree = ""; }; 8D576316048677EA00EA77CD /* PreferencePaneExample.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PreferencePaneExample.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4C51BE9412D0471600E79CEB /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* PreferencePaneExample */ = { isa = PBXGroup; children = ( 4CF30A1920EC73480028D64F /* Classes */, 4CF30A1C20EC73480028D64F /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = PreferencePaneExample; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( 4C51BE9312D0471600E79CEB /* Cocoa.framework */, ); name = Frameworks; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* PreferencePaneExample.bundle */, ); name = Products; sourceTree = ""; }; 4C0BA80C20ECB0CB00C9D351 /* User Interface */ = { isa = PBXGroup; children = ( 4C0BA80D20ECB0CB00C9D351 /* PreferencePane.xib */, ); path = "User Interface"; sourceTree = ""; }; 4C0BA80E20ECB0CB00C9D351 /* Property Lists */ = { isa = PBXGroup; children = ( 4C0BA80F20ECB0CB00C9D351 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4CF30A1920EC73480028D64F /* Classes */ = { isa = PBXGroup; children = ( 4CF30A1A20EC73480028D64F /* TPI_PreferencePaneExample.h */, 4CF30A1B20EC73480028D64F /* TPI_PreferencePaneExample.m */, ); path = Classes; sourceTree = ""; }; 4CF30A1C20EC73480028D64F /* Resources */ = { isa = PBXGroup; children = ( 4C0BA80E20ECB0CB00C9D351 /* Property Lists */, 4C0BA80C20ECB0CB00C9D351 /* User Interface */, ); path = Resources; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* PreferencePaneExample */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "PreferencePaneExample" */; buildPhases = ( 8D57630F048677EA00EA77CD /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = PreferencePaneExample; productInstallPath = "$(HOME)/Library/Bundles"; productName = PreferencePaneExample; productReference = 8D576316048677EA00EA77CD /* PreferencePaneExample.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1000; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "PreferencePaneExample" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 1; knownRegions = ( English, Japanese, French, German, ); mainGroup = 089C166AFE841209C02AAC07 /* PreferencePaneExample */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* PreferencePaneExample */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 8D57630F048677EA00EA77CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C0BA81020ECB0CB00C9D351 /* PreferencePane.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4CF30A1F20EC73480028D64F /* TPI_PreferencePaneExample.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 1DEB911B08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = /Applications/Textual.app/Contents/MacOS/Textual; CLANG_ENABLE_MODULES = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Frameworks/**\""; GCC_OPTIMIZATION_LEVEL = 0; HEADER_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Headers/**\""; INFOPLIST_FILE = "Resources/Property List/Info.plist"; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-PreferencePaneExample"; PRODUCT_NAME = PreferencePaneExample; WRAPPER_EXTENSION = bundle; }; name = Debug; }; 1DEB911C08733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = /Applications/Textual.app/Contents/MacOS/Textual; CLANG_ENABLE_MODULES = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; FRAMEWORK_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Frameworks/**\""; HEADER_SEARCH_PATHS = "\"/Applications/Textual.app/Contents/Headers/**\""; INFOPLIST_FILE = "Resources/Property List/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-PreferencePaneExample"; PRODUCT_NAME = PreferencePaneExample; WRAPPER_EXTENSION = bundle; }; name = Release; }; 1DEB911F08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; 1DEB912008733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "PreferencePaneExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911B08733D790010E9CD /* Debug */, 1DEB911C08733D790010E9CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "PreferencePaneExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911F08733D790010E9CD /* Debug */, 1DEB912008733D790010E9CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Plugins/Sample Code/Preference Pane/Resources/Property Lists/Info.plist ================================================ CFBundleDevelopmentRegion English CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.0 MinimumTextualVersion 6.0.0 NSPrincipalClass TPI_PreferencePaneExample ================================================ FILE: Sources/Plugins/Sample Code/Preference Pane/Resources/User Interface/PreferencePane.xib ================================================ ================================================ FILE: Sources/Plugins/Smiley Converter/Classes/TPISmileyConverter.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2013 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" NS_ASSUME_NONNULL_BEGIN @interface TPISmileyConverter : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Smiley Converter/Classes/TPISmileyConverter.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2013 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPISmileyConverter.h" NS_ASSUME_NONNULL_BEGIN @interface TPISmileyConverter () @property (nonatomic, copy) NSDictionary *conversionTable; @property (nonatomic, copy) NSArray *sortedSmileyList; @property (nonatomic, strong) IBOutlet NSView *preferencesPane; - (IBAction)preferenceChanged:(id)sender; @end @implementation TPISmileyConverter #pragma mark - #pragma mark Plugin API - (void)pluginLoadedIntoMemory { XRPerformBlockSynchronouslyOnMainQueue(^{ [TPIBundleFromClass() loadNibNamed:@"TPISmileyConverter" owner:self topLevelObjects:nil]; }); [self maybeBuildConversionTable]; } - (void)maybeBuildConversionTable { BOOL serviceEnabled = [RZUserDefaults() boolForKey:@"Smiley Converter Extension -> Enable Service"]; if (serviceEnabled == NO) { return; } [self buildConversionTable]; } - (void)buildConversionTable { NSMutableDictionary *conversionTable = [NSMutableDictionary dictionary]; NSURL *tablePath = [TPIBundleFromClass() URLForResource:@"conversionTable" withExtension:@"plist"]; /* Load primary table */ NSDictionary *tableData = [NSDictionary dictionaryWithContentsOfURL:tablePath]; NSAssert((tableData != nil), @"Failed to load conversion table"); [conversionTable addEntriesFromDictionary:tableData]; /* Load larger table */ if ([RZUserDefaults() boolForKey:@"Smiley Converter Extension -> Enable Extra Emoticons"]) { NSURL *tablePath2 = [TPIBundleFromClass() URLForResource:@"conversionTable2" withExtension:@"plist"]; NSDictionary *tableData2 = [NSDictionary dictionaryWithContentsOfURL:tablePath2]; NSAssert((tableData2 != nil), @"Failed to load conversion table"); [conversionTable addEntriesFromDictionary:tableData2]; } /* Save table contents */ self.conversionTable = conversionTable; self.sortedSmileyList = conversionTable.sortedDictionaryKeysReversed; } - (void)destroyConversionTable { self.conversionTable = nil; self.sortedSmileyList = nil; } - (void)preferenceChanged:(id)sender { [self destroyConversionTable]; [self maybeBuildConversionTable]; } - (NSView *)pluginPreferencesPaneView { return self.preferencesPane; } - (NSString *)pluginPreferencesPaneMenuItemName { return TPILocalizedString(@"BasicLanguage[3kj-8f]"); } - (NSString *)willRenderMessage:(NSString *)newMessage forViewController:(TVCLogController *)viewController lineType:(TVCLogLineType)lineType memberType:(TVCLogLineMemberType)memberType { BOOL serviceEnabled = [RZUserDefaults() boolForKey:@"Smiley Converter Extension -> Enable Service"]; if (serviceEnabled == NO) { return newMessage; } if (lineType == TVCLogLineTypeAction || lineType == TVCLogLineTypePrivateMessage) { return [self convertStringToEmoji:newMessage]; } return newMessage; } #pragma mark - #pragma mark Convert API - (NSString *)convertStringToEmoji:(NSString *)string { NSMutableString *finalString = [string mutableCopy]; for (NSString *smiley in self.sortedSmileyList) { [self stringWithReplacedSmiley:smiley inString:finalString]; } return [finalString copy]; } /* The replacement call uses a lot of work done by the actual Textual rendering engine. */ - (void)stringWithReplacedSmiley:(NSString *)smiley inString:(NSMutableString *)inString { NSUInteger currentPosition = 0; while (currentPosition < inString.length) { NSRange range = [inString rangeOfString:smiley options:NSCaseInsensitiveSearch range:NSMakeRange(currentPosition, (inString.length - currentPosition))]; if (range.location == NSNotFound) { break; } BOOL enabled = YES; NSInteger leftLocation = (range.location - 1); if (leftLocation >= 0 && leftLocation < inString.length) { UniChar c = [inString characterAtIndex:leftLocation]; if (c != ' ') { enabled = NO; goto next_pass; } } NSInteger rightLocation = NSMaxRange(range); if (rightLocation < inString.length) { UniChar c = [inString characterAtIndex:rightLocation]; if (c != ' ') { enabled = NO; goto next_pass; } } next_pass: if (enabled) { NSString *emoji = self.conversionTable[smiley]; [inString replaceCharactersInRange:range withString:emoji]; currentPosition = (range.location + emoji.length + 1); } else { currentPosition = (NSMaxRange(range) + 1); } } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/Smiley Converter/Resources/Language Files/en.lproj/BasicLanguage.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2013 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "3kj-8f" = "Smiley Converter"; ================================================ FILE: Sources/Plugins/Smiley Converter/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.1 NSHumanReadableCopyright Copyright © 2013 - 2020 Codeux Software, LLC. All rights reserved. MinimumTextualVersion 7.2.4 NSPrincipalClass TPISmileyConverter ================================================ FILE: Sources/Plugins/Smiley Converter/Resources/Property Lists/conversionTable.plist ================================================ :-) 😊 :) 😊 :o) 😊 :] 😊 :3 😊 :c) 😊 :> 😊 =] 😊 8) 😊 =) 😊 :} 😊 :^) 😊 :っ) 😊 :-D 😃 :D 😃 8-D 😃 8D 😃 x-D 😃 xD 😃 X-D 😃 XD 😃 =-D 😃 =D 😃 =-3 😃 =3 😃 B^D 😃 ;D 😄 ;) 😉 ;-) 😉 ;P 😜 ;p 😜 >:P 😝 :-P 😝 :P 😝 X-P 😝 x-p 😝 :-p 😝 :p 😝 =p 😝 :-Þ 😝 😝 :-b 😝 :b 😝 o_o 😳 O_O 😳 o_O 😳 O_o 😳 0_o 😳 o_0 😳 o.o 😳 0_0 😳 O.O 😳 o.O 😳 O.o 😳 :@ 😡 >.< 😣 >_< 😫 :( 😞 :-( 😞 n_n 😄 u_u 😔 ^_^' 😅 ^_^ 😄 ^.^ 😊 x.x 😵 X.x 😵 X.X 😵 x_x 😵 x_X 😵 X_x 😵 X_X 😵 x.X 😵 DD: 😫 D:< 😧 D: 😧 D; 😧 D= 😧 v.v 😧 D-': 😧 :s 😖 :S 😖 ._.' 😰 ._. 😞 ;_; 😢 ;__; 😢 T_T 😭 T.T 😭 :-| 😐 :| 😐 :o 😯 :-o 😯 :O 😱 :-O 😱 :0 😱 :-0 😱 -_- 😑 -.- 😑 :*** 😘 :** 😘 :* 😚 :/ 😕 :-/ 😕 ;/ 😕 ;-/ 😕 <3 >: 😡 >.> 😒 >_> 😒 <_< 😒 <.< 😒 :goat: 🐐 ================================================ FILE: Sources/Plugins/Smiley Converter/Resources/Property Lists/conversionTable2.plist ================================================ :+1: 👍 :-1: 👎 :100: 💯 :1234: 🔢 :8ball: 🎱 :a: 🅰 :ab: 🆎 :abc: 🔤 :abcd: 🔡 :accept: 🉑 :aerial_tramway: 🚡 :airplane: :alarm_clock: :alien: 👽 :ambulance: 🚑 :anchor: :angel: 👼 :anger: 💢 :angry: 😠 :anguished: 😧 :ant: 🐜 :apple: 🍎 :aquarius: :aries: :arrow_backward: :arrow_double_down: :arrow_double_up: :arrow_down: :arrow_down_small: 🔽 :arrow_forward: :arrow_heading_down: :arrow_heading_up: :arrow_left: :arrow_lower_left: :arrow_lower_right: :arrow_right: :arrow_right_hook: :arrow_up: :arrow_up_down: :arrow_up_small: 🔼 :arrow_upper_left: :arrow_upper_right: :arrows_clockwise: 🔃 :arrows_counterclockwise: 🔄 :art: 🎨 :articulated_lorry: 🚛 :astonished: 😲 :athletic_shoe: 👟 :atm: 🏧 :b: 🅱 :baby: 👶 :baby_bottle: 🍼 :baby_chick: 🐤 :baby_symbol: 🚼 :back: 🔙 :baggage_claim: 🛄 :balloon: 🎈 :ballot_box_with_check: :bamboo: 🎍 :banana: 🍌 :bangbang: :bank: 🏦 :bar_chart: 📊 :barber: 💈 :baseball: :basketball: 🏀 :bath: 🛀 :bathtub: 🛁 :battery: 🔋 :bear: 🐻 :bee: 🐝 :beer: 🍺 :beers: 🍻 :beetle: 🐞 :beginner: 🔰 :bell: 🔔 :bento: 🍱 :bicyclist: 🚴 :bike: 🚲 :bikini: 👙 :bird: 🐦 :birthday: 🎂 :black_circle: :black_joker: 🃏 :black_large_square: :black_medium_small_square: :black_medium_square: :black_nib: :black_small_square: :black_square_button: 🔲 :blossom: 🌼 :blowfish: 🐡 :blue_book: 📘 :blue_car: 🚙 :blue_heart: 💙 :blush: 😊 :boar: 🐗 :boat: :bomb: 💣 :book: 📖 :bookmark: 🔖 :bookmark_tabs: 📑 :books: 📚 :boom: 💥 :boot: 👢 :bouquet: 💐 :bow: 🙇 :bowling: 🎳 :boy: 👦 :bread: 🍞 :bride_with_veil: 👰 :bridge_at_night: 🌉 :briefcase: 💼 :broken_heart: 💔 :bug: 🐛 :bulb: 💡 :bullettrain_front: 🚅 :bullettrain_side: 🚄 :bus: 🚌 :busstop: 🚏 :bust_in_silhouette: 👤 :busts_in_silhouette: 👥 :cactus: 🌵 :cake: 🍰 :calendar: 📆 :calling: 📲 :camel: 🐫 :camera: 📷 :cancer: :candy: 🍬 :capital_abcd: 🔠 :capricorn: :car: 🚗 :card_index: 📇 :carousel_horse: 🎠 :cat2: 🐈 :cat: 🐱 :cd: 💿 :chart: 💹 :chart_with_downwards_trend: 📉 :chart_with_upwards_trend: 📈 :checkered_flag: 🏁 :cherries: 🍒 :cherry_blossom: 🌸 :chestnut: 🌰 :chicken: 🐔 :children_crossing: 🚸 :chocolate_bar: 🍫 :christmas_tree: 🎄 :church: :cinema: 🎦 :circus_tent: 🎪 :city_sunrise: 🌇 :city_sunset: 🌆 :cl: 🆑 :clap: 👏 :clapper: 🎬 :clipboard: 📋 :clock1030: 🕥 :clock10: 🕙 :clock1130: 🕦 :clock11: 🕚 :clock1230: 🕧 :clock12: 🕛 :clock130: 🕜 :clock1: 🕐 :clock230: 🕝 :clock2: 🕑 :clock330: 🕞 :clock3: 🕒 :clock430: 🕟 :clock4: 🕓 :clock530: 🕠 :clock5: 🕔 :clock630: 🕡 :clock6: 🕕 :clock730: 🕢 :clock7: 🕖 :clock830: 🕣 :clock8: 🕗 :clock930: 🕤 :clock9: 🕘 :closed_book: 📕 :closed_lock_with_key: 🔐 :closed_umbrella: 🌂 :cloud: :clubs: :cocktail: 🍸 :coffee: :cold_sweat: 😰 :collision: 💥 :computer: 💻 :confetti_ball: 🎊 :confounded: 😖 :confused: 😕 :congratulations: :construction: 🚧 :construction_worker: 👷 :convenience_store: 🏪 :cookie: 🍪 :cool: 🆒 :cop: 👮 :copyright: © :corn: 🌽 :couple: 👫 :couple_with_heart: 💑 :couplekiss: 💏 :cow2: 🐄 :cow: 🐮 :credit_card: 💳 :crescent_moon: 🌙 :crocodile: 🐊 :crossed_flags: 🎌 :crown: 👑 :cry: 😢 :crying_cat_face: 😿 :crystal_ball: 🔮 :cupid: 💘 :curly_loop: :currency_exchange: 💱 :curry: 🍛 :custard: 🍮 :customs: 🛃 :cyclone: 🌀 :dancer: 💃 :dancers: 👯 :dango: 🍡 :dart: 🎯 :dash: 💨 :date: 📅 :deciduous_tree: 🌳 :department_store: 🏬 :diamond_shape_with_a_dot_inside: 💠 :diamonds: :disappointed: 😞 :disappointed_relieved: 😥 :dizzy: 💫 :dizzy_face: 😵 :do_not_litter: 🚯 :dog2: 🐕 :dog: 🐶 :dollar: 💵 :dolls: 🎎 :dolphin: 🐬 :door: 🚪 :doughnut: 🍩 :dragon: 🐉 :dragon_face: 🐲 :dress: 👗 :dromedary_camel: 🐪 :droplet: 💧 :dvd: 📀 :e-mail: 📧 :ear: 👂 :ear_of_rice: 🌾 :earth_africa: 🌍 :earth_americas: 🌎 :earth_asia: 🌏 :egg: 🍳 :eggplant: 🍆 :eight_pointed_black_star: :eight_spoked_asterisk: :electric_plug: 🔌 :elephant: 🐘 :email: :end: 🔚 :envelope: :envelope_with_arrow: 📩 :euro: 💶 :european_castle: 🏰 :european_post_office: 🏤 :evergreen_tree: 🌲 :exclamation: :expressionless: 😑 :eyeglasses: 👓 :eyes: 👀 :facepunch: 👊 :factory: 🏭 :fallen_leaf: 🍂 :family: 👪 :fast_forward: :fax: 📠 :fearful: 😨 :feet: 🐾 :ferris_wheel: 🎡 :file_folder: 📁 :fire: 🔥 :fire_engine: 🚒 :fireworks: 🎆 :first_quarter_moon: 🌓 :first_quarter_moon_with_face: 🌛 :fish: 🐟 :fish_cake: 🍥 :fishing_pole_and_fish: 🎣 :fist: :flags: 🎏 :flashlight: 🔦 :flipper: 🐬 :floppy_disk: 💾 :flower_playing_cards: 🎴 :flushed: 😳 :foggy: 🌁 :football: 🏈 :footprints: 👣 :fork_and_knife: 🍴 :fountain: :four_leaf_clover: 🍀 :free: 🆓 :fried_shrimp: 🍤 :fries: 🍟 :frog: 🐸 :frowning: 😦 :fuelpump: :full_moon: 🌕 :full_moon_with_face: 🌝 :game_die: 🎲 :gem: 💎 :gemini: :ghost: 👻 :gift: 🎁 :gift_heart: 💝 :girl: 👧 :globe_with_meridians: 🌐 :goat: 🐐 :golf: :grapes: 🍇 :green_apple: 🍏 :green_book: 📗 :green_heart: 💚 :grey_exclamation: :grey_question: :grimacing: 😬 :grin: 😁 :grinning: 😀 :guardsman: 💂 :guitar: 🎸 :gun: 🔫 :haircut: 💇 :hamburger: 🍔 :hammer: 🔨 :hamster: 🐹 :hand: :handbag: 👜 :hankey: 💩 :hatched_chick: 🐥 :hatching_chick: 🐣 :headphones: 🎧 :hear_no_evil: 🙉 :heart: :heart_decoration: 💟 :heart_eyes: 😍 :heart_eyes_cat: 😻 :heartbeat: 💓 :heartpulse: 💗 :hearts: :heavy_check_mark: :heavy_division_sign: :heavy_dollar_sign: 💲 :heavy_exclamation_mark: :heavy_minus_sign: :heavy_multiplication_x: :heavy_plus_sign: :helicopter: 🚁 :herb: 🌿 :hibiscus: 🌺 :high_brightness: 🔆 :high_heel: 👠 :hocho: 🔪 :honey_pot: 🍯 :honeybee: 🐝 :horse: 🐴 :horse_racing: 🏇 :hospital: 🏥 :hotel: 🏨 :hotsprings: :hourglass: :hourglass_flowing_sand: :house: 🏠 :house_with_garden: 🏡 :hushed: 😯 :ice_cream: 🍨 :icecream: 🍦 :id: 🆔 :ideograph_advantage: 🉐 :imp: 👿 :inbox_tray: 📥 :incoming_envelope: 📨 :information_desk_person: 💁 :information_source: :innocent: 😇 :interrobang: :iphone: 📱 :izakaya_lantern: 🏮 :jack_o_lantern: 🎃 :japan: 🗾 :japanese_castle: 🏯 :japanese_goblin: 👺 :japanese_ogre: 👹 :jeans: 👖 :joy: 😂 :joy_cat: 😹 :key: 🔑 :keycap_ten: 🔟 :kimono: 👘 :kiss: 💋 :kissing: 😗 :kissing_cat: 😽 :kissing_closed_eyes: 😚 :kissing_heart: 😘 :kissing_smiling_eyes: 😙 :koala: 🐨 :koko: 🈁 :lantern: 🏮 :large_blue_circle: 🔵 :large_blue_diamond: 🔷 :large_orange_diamond: 🔶 :last_quarter_moon: 🌗 :last_quarter_moon_with_face: 🌜 :laughing: 😆 :leaves: 🍃 :ledger: 📒 :left_luggage: 🛅 :left_right_arrow: :leftwards_arrow_with_hook: :lemon: 🍋 :leo: :leopard: 🐆 :libra: :light_rail: 🚈 :link: 🔗 :lips: 👄 :lipstick: 💄 :lock: 🔒 :lock_with_ink_pen: 🔏 :lollipop: 🍭 :loop: :loudspeaker: 📢 :love_hotel: 🏩 :love_letter: 💌 :low_brightness: 🔅 :m: :mag: 🔍 :mag_right: 🔎 :mahjong: 🀄 :mailbox: 📫 :mailbox_closed: 📪 :mailbox_with_mail: 📬 :mailbox_with_no_mail: 📭 :man: 👨 :man_with_gua_pi_mao: 👲 :man_with_turban: 👳 :mans_shoe: 👞 :maple_leaf: 🍁 :mask: 😷 :massage: 💆 :meat_on_bone: 🍖 :mega: 📣 :melon: 🍈 :memo: 📝 :mens: 🚹 :metro: 🚇 :microphone: 🎤 :microscope: 🔬 :milky_way: 🌌 :minibus: 🚐 :minidisc: 💽 :mobile_phone_off: 📴 :money_with_wings: 💸 :moneybag: 💰 :monkey: 🐒 :monkey_face: 🐵 :monorail: 🚝 :moon: 🌔 :mortar_board: 🎓 :mount_fuji: 🗻 :mountain_bicyclist: 🚵 :mountain_cableway: 🚠 :mountain_railway: 🚞 :mouse2: 🐁 :mouse: 🐭 :movie_camera: 🎥 :moyai: 🗿 :muscle: 💪 :mushroom: 🍄 :musical_keyboard: 🎹 :musical_note: 🎵 :musical_score: 🎼 :mute: 🔇 :nail_care: 💅 :name_badge: 📛 :necktie: 👔 :negative_squared_cross_mark: :neutral_face: 😐 :new: 🆕 :new_moon: 🌑 :new_moon_with_face: 🌚 :newspaper: 📰 :ng: 🆖 :no_bell: 🔕 :no_bicycles: 🚳 :no_entry: :no_entry_sign: 🚫 :no_good: 🙅 :no_mobile_phones: 📵 :no_mouth: 😶 :no_pedestrians: 🚷 :no_smoking: 🚭 :non-potable_water: 🚱 :nose: 👃 :notebook: 📓 :notebook_with_decorative_cover: 📔 :notes: 🎶 :nut_and_bolt: 🔩 :o2: 🅾 :o: :ocean: 🌊 :octopus: 🐙 :oden: 🍢 :office: 🏢 :ok: 🆗 :ok_hand: 👌 :ok_woman: 🙆 :older_man: 👴 :older_woman: 👵 :on: 🔛 :oncoming_automobile: 🚘 :oncoming_bus: 🚍 :oncoming_police_car: 🚔 :oncoming_taxi: 🚖 :open_book: 📖 :open_file_folder: 📂 :open_hands: 👐 :open_mouth: 😮 :ophiuchus: :orange_book: 📙 :outbox_tray: 📤 :ox: 🐂 :package: 📦 :page_facing_up: 📄 :page_with_curl: 📃 :pager: 📟 :palm_tree: 🌴 :panda_face: 🐼 :paperclip: 📎 :parking: 🅿 :part_alternation_mark: :partly_sunny: :passport_control: 🛂 :paw_prints: 🐾 :peach: 🍑 :pear: 🍐 :pencil2: :pencil: 📝 :penguin: 🐧 :pensive: 😔 :performing_arts: 🎭 :persevere: 😣 :person_frowning: 🙍 :person_with_blond_hair: 👱 :person_with_pouting_face: 🙎 :phone: :pig2: 🐖 :pig: 🐷 :pig_nose: 🐽 :pill: 💊 :pineapple: 🍍 :pisces: :pizza: 🍕 :point_down: 👇 :point_left: 👈 :point_right: 👉 :point_up: :point_up_2: 👆 :police_car: 🚓 :poodle: 🐩 :poop: 💩 :post_office: 🏣 :postal_horn: 📯 :postbox: 📮 :potable_water: 🚰 :pouch: 👝 :poultry_leg: 🍗 :pound: 💷 :pouting_cat: 😾 :pray: 🙏 :princess: 👸 :punch: 👊 :purple_heart: 💜 :purse: 👛 :pushpin: 📌 :put_litter_in_its_place: 🚮 :question: :rabbit2: 🐇 :rabbit: 🐰 :racehorse: 🐎 :radio: 📻 :radio_button: 🔘 :rage: 😡 :railway_car: 🚃 :rainbow: 🌈 :raised_hand: :raised_hands: 🙌 :raising_hand: 🙋 :ram: 🐏 :ramen: 🍜 :rat: 🐀 :recycle: :red_car: 🚗 :red_circle: 🔴 :registered: ® :relaxed: :relieved: 😌 :repeat: 🔁 :repeat_one: 🔂 :restroom: 🚻 :revolving_hearts: 💞 :rewind: :ribbon: 🎀 :rice: 🍚 :rice_ball: 🍙 :rice_cracker: 🍘 :rice_scene: 🎑 :ring: 💍 :rocket: 🚀 :roller_coaster: 🎢 :rooster: 🐓 :rose: 🌹 :rotating_light: 🚨 :round_pushpin: 📍 :rowboat: 🚣 :rugby_football: 🏉 :runner: 🏃 :running: 🏃 :running_shirt_with_sash: 🎽 :sa: 🈂 :sagittarius: :sailboat: :sake: 🍶 :sandal: 👡 :santa: 🎅 :satellite: 📡 :satisfied: 😆 :saxophone: 🎷 :school: 🏫 :school_satchel: 🎒 :scissors: :scorpius: :scream: 😱 :scream_cat: 🙀 :scroll: 📜 :seat: 💺 :secret: :see_no_evil: 🙈 :seedling: 🌱 :shaved_ice: 🍧 :sheep: 🐑 :shell: 🐚 :ship: 🚢 :shirt: 👕 :shit: 💩 :simple_smile: 😊 :shoe: 👞 :shower: 🚿 :signal_strength: 📶 :six_pointed_star: 🔯 :ski: 🎿 :skull: 💀 :sleeping: 😴 :sleepy: 😪 :slot_machine: 🎰 :small_blue_diamond: 🔹 :small_orange_diamond: 🔸 :small_red_triangle: 🔺 :small_red_triangle_down: 🔻 :smile: 😄 :smile_cat: 😸 :smiley: 😃 :smiley_cat: 😺 :smiling_imp: 😈 :smirk: 😏 :smirk_cat: 😼 :smoking: 🚬 :snail: 🐌 :snake: 🐍 :snowboarder: 🏂 :snowflake: :snowman: :sob: 😭 :soccer: :soon: 🔜 :sos: 🆘 :sound: 🔉 :space_invader: 👾 :spades: :spaghetti: 🍝 :sparkle: :sparkler: 🎇 :sparkles: :sparkling_heart: 💖 :speak_no_evil: 🙊 :speaker: 🔊 :speech_balloon: 💬 :speedboat: 🚤 :star2: 🌟 :star: :stars: 🌃 :station: 🚉 :statue_of_liberty: 🗽 :steam_locomotive: 🚂 :stew: 🍲 :straight_ruler: 📏 :strawberry: 🍓 :stuck_out_tongue: 😛 :stuck_out_tongue_closed_eyes: 😝 :stuck_out_tongue_winking_eye: 😜 :sun_with_face: 🌞 :sunflower: 🌻 :sunglasses: 😎 :sunny: :sunrise: 🌅 :sunrise_over_mountains: 🌄 :surfer: 🏄 :sushi: 🍣 :suspension_railway: 🚟 :sweat: 😓 :sweat_drops: 💦 :sweat_smile: 😅 :sweet_potato: 🍠 :swimmer: 🏊 :symbols: 🔣 :syringe: 💉 :tada: 🎉 :tanabata_tree: 🎋 :tangerine: 🍊 :taurus: :taxi: 🚕 :tea: 🍵 :telephone: :telephone_receiver: 📞 :telescope: 🔭 :tennis: 🎾 :tent: :thinking: 🤔 :thought_balloon: 💭 :thumbsdown: 👎 :thumbsup: 👍 :ticket: 🎫 :tiger2: 🐅 :tiger: 🐯 :tired_face: 😫 :tm: :toilet: 🚽 :tokyo_tower: 🗼 :tomato: 🍅 :tongue: 👅 :top: 🔝 :tophat: 🎩 :tractor: 🚜 :traffic_light: 🚥 :train2: 🚆 :train: 🚃 :tram: 🚊 :triangular_flag_on_post: 🚩 :triangular_ruler: 📐 :trident: 🔱 :triumph: 😤 :trolleybus: 🚎 :trophy: 🏆 :tropical_drink: 🍹 :tropical_fish: 🐠 :truck: 🚚 :trumpet: 🎺 :tshirt: 👕 :tulip: 🌷 :turtle: 🐢 :tv: 📺 :twisted_rightwards_arrows: 🔀 :two_hearts: 💕 :two_men_holding_hands: 👬 :two_women_holding_hands: 👭 :u5272: 🈹 :u5408: 🈴 :u55b6: 🈺 :u6307: 🈯 :u6708: 🈷 :u6709: 🈶 :u6e80: 🈵 :u7121: 🈚 :u7533: 🈸 :u7981: 🈲 :u7a7a: 🈳 :umbrella: :unamused: 😒 :underage: 🔞 :unlock: 🔓 :up: 🆙 :v: :vertical_traffic_light: 🚦 :vhs: 📼 :vibration_mode: 📳 :video_camera: 📹 :video_game: 🎮 :violin: 🎻 :virgo: :volcano: 🌋 :vs: 🆚 :walking: 🚶 :waning_crescent_moon: 🌘 :waning_gibbous_moon: 🌖 :warning: :watch: :water_buffalo: 🐃 :watermelon: 🍉 :wave: 👋 :wavy_dash: :waxing_crescent_moon: 🌒 :waxing_gibbous_moon: 🌔 :wc: 🚾 :weary: 😩 :wedding: 💒 :whale2: 🐋 :whale: 🐳 :wheelchair: :white_check_mark: :white_circle: :white_flower: 💮 :white_large_square: :white_medium_small_square: :white_medium_square: :white_small_square: :white_square_button: 🔳 :wind_chime: 🎐 :wine_glass: 🍷 :wink: 😉 :wolf: 🐺 :woman: 👩 :womans_clothes: 👚 :womans_hat: 👒 :womens: 🚺 :worried: 😟 :wrench: 🔧 :x: :yellow_heart: 💛 :yen: 💴 :yum: 😋 :zap: :zzz: 💤 ================================================ FILE: Sources/Plugins/Smiley Converter/Resources/User Interface/en.lproj/TPISmileyConverter.xib ================================================ ================================================ FILE: Sources/Plugins/Smiley Converter/Smiley Converter Extension.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C36AADF20F27D8E007CA939 /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AADD20F27D8E007CA939 /* BasicLanguage.strings */; }; 4C36AAE220F27D9A007CA939 /* TPISmileyConverter.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAE020F27D9A007CA939 /* TPISmileyConverter.xib */; }; 4C46A0C820EC6BA900094EA4 /* TPISmileyConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A0C620EC6BA900094EA4 /* TPISmileyConverter.m */; }; 4C46A0D220EC6BB700094EA4 /* conversionTable2.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C46A0CA20EC6BB700094EA4 /* conversionTable2.plist */; }; 4C46A0D320EC6BB700094EA4 /* conversionTable.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C46A0CB20EC6BB700094EA4 /* conversionTable.plist */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C1D10E820EBF45200E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D10E920EBF45200E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D10ED20EBF45200E72D80 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C1D10FB20EBF45200E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D10FC20EBF45200E72D80 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4C1D10FD20EBF45200E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D10FF20EBF45200E72D80 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4C1D110220EBF45200E72D80 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4C1D111020EBF45200E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D111120EBF45200E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D111520EBF45200E72D80 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C36AADE20F27D8E007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BasicLanguage.strings; sourceTree = ""; }; 4C36AAE120F27D9A007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TPISmileyConverter.xib; sourceTree = ""; }; 4C46A0C620EC6BA900094EA4 /* TPISmileyConverter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPISmileyConverter.m; sourceTree = ""; }; 4C46A0C720EC6BA900094EA4 /* TPISmileyConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPISmileyConverter.h; sourceTree = ""; }; 4C46A0CA20EC6BB700094EA4 /* conversionTable2.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = conversionTable2.plist; sourceTree = ""; }; 4C46A0CB20EC6BB700094EA4 /* conversionTable.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = conversionTable.plist; sourceTree = ""; }; 4C46A0CC20EC6BB700094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8D576316048677EA00EA77CD /* Smiley Converter.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Smiley Converter.bundle"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* BragSpam */ = { isa = PBXGroup; children = ( 4C46A0C520EC6BA900094EA4 /* Classes */, 089C167CFE841241C02AAC07 /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = BragSpam; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; 089C167CFE841241C02AAC07 /* Resources */ = { isa = PBXGroup; children = ( 4C69189716E3276C00BDE3A8 /* Configurations */, 4C46A0D020EC6BB700094EA4 /* Language Files */, 4C46A0C920EC6BB700094EA4 /* Property Lists */, 4C46A0CD20EC6BB700094EA4 /* User Interface */, ); path = Resources; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* Smiley Converter.bundle */, ); name = Products; sourceTree = ""; }; 4C1D10E420EBF45200E72D80 /* Build */ = { isa = PBXGroup; children = ( 4C1D10F820EBF45200E72D80 /* Common */, 4C1D110D20EBF45200E72D80 /* Debug */, 4C1D10E520EBF45200E72D80 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4C1D10E520EBF45200E72D80 /* Standard Release */ = { isa = PBXGroup; children = ( 4C1D10ED20EBF45200E72D80 /* Enabled Features.xcconfig */, 4C1D10E920EBF45200E72D80 /* Textual Extensions.xcconfig */, 4C1D10E820EBF45200E72D80 /* Textual.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4C1D10F820EBF45200E72D80 /* Common */ = { isa = PBXGroup; children = ( 4C1D110220EBF45200E72D80 /* Foundation Debug.xcconfig */, 4C1D10FF20EBF45200E72D80 /* Foundation.xcconfig */, 4C1D10FC20EBF45200E72D80 /* Preserve Symbols.xcconfig */, 4C1D10FD20EBF45200E72D80 /* Textual Extensions.xcconfig */, 4C1D10FB20EBF45200E72D80 /* Textual.xcconfig */, ); path = Common; sourceTree = ""; }; 4C1D110D20EBF45200E72D80 /* Debug */ = { isa = PBXGroup; children = ( 4C1D111520EBF45200E72D80 /* Enabled Features.xcconfig */, 4C1D111120EBF45200E72D80 /* Textual Extensions.xcconfig */, 4C1D111020EBF45200E72D80 /* Textual.xcconfig */, ); path = Debug; sourceTree = ""; }; 4C46A0C520EC6BA900094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A0C720EC6BA900094EA4 /* TPISmileyConverter.h */, 4C46A0C620EC6BA900094EA4 /* TPISmileyConverter.m */, ); path = Classes; sourceTree = ""; }; 4C46A0C920EC6BB700094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A0CB20EC6BB700094EA4 /* conversionTable.plist */, 4C46A0CA20EC6BB700094EA4 /* conversionTable2.plist */, 4C46A0CC20EC6BB700094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A0CD20EC6BB700094EA4 /* User Interface */ = { isa = PBXGroup; children = ( 4C36AAE020F27D9A007CA939 /* TPISmileyConverter.xib */, ); path = "User Interface"; sourceTree = ""; }; 4C46A0D020EC6BB700094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C36AADD20F27D8E007CA939 /* BasicLanguage.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C69189716E3276C00BDE3A8 /* Configurations */ = { isa = PBXGroup; children = ( 4C1D10E420EBF45200E72D80 /* Build */, ); name = Configurations; path = ../../../Configurations; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* Smiley Converter Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Smiley Converter Extension" */; buildPhases = ( 8D57630F048677EA00EA77CD /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "Smiley Converter Extension"; productInstallPath = "$(HOME)/Library/Bundles"; productName = BragSpam; productReference = 8D576316048677EA00EA77CD /* Smiley Converter.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; TargetAttributes = { 8D57630D048677EA00EA77CD = { DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Smiley Converter Extension" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( en, Base, ); mainGroup = 089C166AFE841209C02AAC07 /* BragSpam */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* Smiley Converter Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 8D57630F048677EA00EA77CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A0D220EC6BB700094EA4 /* conversionTable2.plist in Resources */, 4C46A0D320EC6BB700094EA4 /* conversionTable.plist in Resources */, 4C36AADF20F27D8E007CA939 /* BasicLanguage.strings in Resources */, 4C36AAE220F27D9A007CA939 /* TPISmileyConverter.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A0C820EC6BA900094EA4 /* TPISmileyConverter.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C36AADD20F27D8E007CA939 /* BasicLanguage.strings */ = { isa = PBXVariantGroup; children = ( 4C36AADE20F27D8E007CA939 /* en */, ); name = BasicLanguage.strings; sourceTree = ""; }; 4C36AAE020F27D9A007CA939 /* TPISmileyConverter.xib */ = { isa = PBXVariantGroup; children = ( 4C36AAE120F27D9A007CA939 /* en */, ); name = TPISmileyConverter.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1DEB911B08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-SmileyConverter"; PRODUCT_NAME = "Smiley Converter"; }; name = Debug; }; 1DEB911C08733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-SmileyConverter"; PRODUCT_NAME = "Smiley Converter"; }; name = Release; }; 1DEB911F08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C1D111120EBF45200E72D80 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Debug; }; 1DEB912008733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C1D10E920EBF45200E72D80 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4C9580121FA41EA700F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (App Store)"; }; 4C9580131FA41EA700F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-SmileyConverter"; PRODUCT_NAME = "Smiley Converter"; }; name = "Release (App Store)"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Smiley Converter Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911B08733D790010E9CD /* Debug */, 1DEB911C08733D790010E9CD /* Release */, 4C9580131FA41EA700F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Smiley Converter Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911F08733D790010E9CD /* Debug */, 1DEB912008733D790010E9CD /* Release */, 4C9580121FA41EA700F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Plugins/System Profiler/Classes/SystemProfiler.h ================================================ #import #import "Textual.h" #include #include #include #include #import #include #include #include #include #include #include #include ================================================ FILE: Sources/Plugins/System Profiler/Classes/TPISystemProfiler.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2012 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #include "SystemProfiler.h" NS_ASSUME_NONNULL_BEGIN @interface TPISystemProfiler : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/System Profiler/Classes/TPISystemProfiler.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2012 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPISystemProfiler.h" #import "TPI_SP_SysInfo.h" NS_ASSUME_NONNULL_BEGIN @interface TPISystemProfiler () @property (nonatomic, strong) NSView *preferencePaneView; @end @implementation TPISystemProfiler #pragma mark - #pragma mark Memory Allocation & Deallocation /* Allocation & Deallocation */ - (void)pluginLoadedIntoMemory { XRPerformBlockSynchronouslyOnMainQueue(^{ NSDictionary *defaults = @{ @"System Profiler Extension -> Feature Disabled -> GPU Model" : @(YES), @"System Profiler Extension -> Feature Disabled -> Disk Information" : @(YES), @"System Profiler Extension -> Feature Disabled -> System Uptime" : @(YES), @"System Profiler Extension -> Feature Disabled -> Memory Information" : @(YES), @"System Profiler Extension -> Feature Disabled -> Screen Resolution" : @(YES), }; [RZUserDefaults() registerDefaults:defaults]; [TPIBundleFromClass() loadNibNamed:@"TPISystemProfiler" owner:self topLevelObjects:nil]; }); } #pragma mark - #pragma mark Preference Pane /* Preference Pane */ - (NSView *)pluginPreferencesPaneView { return self.preferencePaneView; } - (NSString *)pluginPreferencesPaneMenuItemName { return TPILocalizedString(@"BasicLanguage[dff-13]"); } #pragma mark - #pragma mark User Input - (NSArray *)subscribedUserInputCommands { return @[@"sysinfo", @"memory", @"uptime", @"netstats", @"msgcount", @"diskspace", @"style", @"screens", @"runcount", @"sysmem"]; } - (void)printDebugInformation:(NSString *)message onClient:(IRCClient *)client inChannel:(IRCChannel *)channel { NSArray *messages = [message componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; for (NSString *messageSplit in messages) { [client printDebugInformation:messageSplit inChannel:channel]; } } - (void)sendMessage:(NSString *)message onClient:(IRCClient *)client toChannel:(IRCChannel *)channel { NSArray *messages = [message componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; for (NSString *messageSplit in messages) { [client sendPrivmsg:messageSplit toChannel:channel]; } } - (void)userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString { XRPerformBlockAsynchronouslyOnMainQueue(^{ [self _userInputCommandInvokedOnClient:client commandString:commandString messageString:messageString]; }); } - (void)_userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString { IRCChannel *channel = mainWindow().selectedChannel; if (channel == nil) { return; } BOOL quietMessage = [messageString isEqualIgnoringCase:@"quiet"]; NSString *messageOut = nil; if ([commandString isEqualToString:@"SYSINFO"]) { messageOut = [TPI_SP_CompiledOutput systemInformation]; } else if ([commandString isEqualToString:@"MEMORY"]) { NSString *memoryMessage = [TPI_SP_CompiledOutput applicationMemoryUsage]; if (quietMessage) { [self printDebugInformation:memoryMessage onClient:client inChannel:channel]; } else { [self sendMessage:memoryMessage onClient:client toChannel:channel]; } NSString *webKitMemoryUse = [TPI_SP_CompiledOutput webKitFrameworkMemoryUsage]; if (webKitMemoryUse) { [self printDebugInformation:webKitMemoryUse onClient:client inChannel:channel]; } } else if ([commandString isEqualToString:@"UPTIME"]) { messageOut = [TPI_SP_CompiledOutput applicationAndSystemUptime]; } else if ([commandString isEqualToString:@"NETSTATS"]) { messageOut = [TPI_SP_CompiledOutput systemNetworkInformation]; } else if ([commandString isEqualToString:@"MSGCOUNT"]) { messageOut = [TPI_SP_CompiledOutput applicationBandwidthStatistics]; } else if ([commandString isEqualToString:@"DISKSPACE"]) { messageOut = [TPI_SP_CompiledOutput systemDiskspaceInformation]; } else if ([commandString isEqualToString:@"STYLE"]) { messageOut = [TPI_SP_CompiledOutput applicationActiveStyle]; } else if ([commandString isEqualToString:@"SCREENS"]) { messageOut = [TPI_SP_CompiledOutput systemDisplayInformation]; } else if ([commandString isEqualToString:@"RUNCOUNT"]) { messageOut = [TPI_SP_CompiledOutput applicationRuntimeStatistics]; } else if ([commandString isEqualToString:@"SYSMEM"]) { messageOut = [TPI_SP_CompiledOutput systemMemoryInformation]; } if (messageOut) { if (quietMessage) { [self printDebugInformation:messageOut onClient:client inChannel:channel]; } else { [self sendMessage:messageOut onClient:client toChannel:channel]; } } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/System Profiler/Classes/TPI_SP_SysInfo.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2012 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #include "SystemProfiler.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_SP_CompiledOutput : NSObject + (NSString *)applicationActiveStyle; + (NSString *)applicationAndSystemUptime; + (NSString *)applicationBandwidthStatistics; + (NSString *)applicationMemoryUsage; + (NSString *)applicationRuntimeStatistics; + (NSString *)systemDiskspaceInformation; + (NSString *)systemDisplayInformation; + (NSString *)systemInformation; + (NSString *)systemMemoryInformation; + (NSString *)systemNetworkInformation; + (nullable NSString *)webKitFrameworkMemoryUsage; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/System Profiler/Classes/TPI_SP_SysInfo.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2012 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "TPI_SP_SysInfo.h" NS_ASSUME_NONNULL_BEGIN #define _localVolumeBaseDirectory @"/Volumes" #define _systemMemoryDivisor 1.073741824 @interface WKWebView () @property (nonatomic, readonly) pid_t _webProcessIdentifier; @end @interface TPI_SP_WebViewProcessInfo : NSObject @property (nonatomic, assign) pid_t processIdentifier; @property (nonatomic, assign) uint64_t processMemoryUse; @property (nonatomic, strong) NSArray *processViewNames; @end @interface TPI_SP_SysInfo : NSObject + (nullable NSString *)modelIdentifier; + (nullable NSString *)processor; + (NSUInteger)processorPhysicalCoreCount; + (NSUInteger)processorVirtualCoreCount; + (nullable NSString *)processorClockSpeed; + (NSTimeInterval)systemUptime; + (NSTimeInterval)applicationUptime; + (uint64_t)freeMemorySize; + (uint64_t)totalMemorySize; + (uint64_t)applicationMemoryInformation; + (nullable NSString *)formattedGraphicsCardInformation; + (nullable NSString *)formattedLocalVolumeDiskUsage; + (NSString *)formattedTotalMemorySize; + (NSString *)formattedDiskSize:(uint64_t)diskSize; + (NSString *)formattedCPUFrequency:(double)frequency; + (NSString *)descriptionForSidebarAppearance; + (NSString *)descriptionForThemeAppearance; + (NSString *)descriptionForThemeAppearance:(TPCThemeAppearanceType)appearance; + (uint64_t)memoryUseForProcess:(pid_t)processIdentifier; + (NSArray *)webViewProcessIdentifiers; + (pid_t)webViewProcessIdentifierForTreeItem:(IRCTreeItem *)treeItem; + (nullable NSString *)refreshRateForScreen:(NSScreen *)screen; @end @implementation TPI_SP_CompiledOutput + (NSString *)applicationActiveStyle { NSString *themeName = themeController().name; TPCThemeStorageLocation storageLocation = themeController().storageLocation; NSString *storageLocationLabel = [TPCThemeController descriptionForStorageLocation:storageLocation]; NSString *sidebarAppearance = [TPI_SP_SysInfo descriptionForSidebarAppearance]; NSString *themeAppearance = [TPI_SP_SysInfo descriptionForThemeAppearance]; NSString *appearanceText = nil; if ([sidebarAppearance isEqualToString:themeAppearance]) { appearanceText = TPILocalizedString(@"BasicLanguage[614-dj]", sidebarAppearance); } else { appearanceText = TPILocalizedString(@"BasicLanguage[843-z4]", themeAppearance, sidebarAppearance); } return TPILocalizedString(@"BasicLanguage[z37-85]", themeName, storageLocationLabel, appearanceText); } + (NSString *)applicationAndSystemUptime { NSUInteger dateFormat = (NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond); NSString *systemUptime = TXHumanReadableTimeInterval([TPI_SP_SysInfo systemUptime], NO, dateFormat); NSString *textualUptime = TXHumanReadableTimeInterval([TPI_SP_SysInfo applicationUptime], NO, dateFormat); return TPILocalizedString(@"BasicLanguage[v03-jx]", systemUptime, textualUptime); } + (NSString *)applicationBandwidthStatistics { IRCClient *client = mainWindow().selectedClient; NSTimeInterval lastMessage = [NSDate timeIntervalSinceNow:client.lastMessageReceived]; return TPILocalizedString(@"BasicLanguage[rua-9r]", TXFormattedNumber(worldController().messagesSent), TXFormattedNumber(worldController().messagesReceived), TXHumanReadableTimeInterval(lastMessage, YES, NSCalendarUnitSecond), [TPI_SP_SysInfo formattedDiskSize:worldController().bandwidthIn], [TPI_SP_SysInfo formattedDiskSize:worldController().bandwidthOut]); } + (NSString *)applicationMemoryUsage { NSUInteger totalScrollbackSize = 0; for (IRCClient *u in worldController().clientList) { totalScrollbackSize += u.viewController.numberOfLines; for (IRCChannel *c in u.channelList) { totalScrollbackSize += c.viewController.numberOfLines; } } uint64_t textualMemoryUse = [TPI_SP_SysInfo applicationMemoryInformation]; return TPILocalizedString(@"BasicLanguage[scn-br]", [TPI_SP_SysInfo formattedDiskSize:textualMemoryUse], TXFormattedNumber(totalScrollbackSize)); } + (nullable NSString *)webKitFrameworkMemoryUsage { NSArray *webViewProcesses = [TPI_SP_SysInfo webViewProcessIdentifiers]; if (webViewProcesses.count == 0) { return nil; } TPI_SP_WebViewProcessInfo *topProcess = webViewProcesses[0]; NSArray *viewNameArray = topProcess.processViewNames; if (viewNameArray.count == 0) { return nil; } NSString *viewName = [viewNameArray componentsJoinedByString:@", "]; NSMutableString *resultString = [NSMutableString string]; if (viewNameArray.count == 1) { [resultString appendString: TPILocalizedString(@"BasicLanguage[ioy-qc]", topProcess.processIdentifier, viewName, [TPI_SP_SysInfo formattedDiskSize:topProcess.processMemoryUse])]; } else { [resultString appendString: TPILocalizedString(@"BasicLanguage[a00-8n]", topProcess.processIdentifier, viewName, [TPI_SP_SysInfo formattedDiskSize:topProcess.processMemoryUse])]; } [resultString appendString:@"\n"]; uint64_t totalMemoryUse = 0; for (TPI_SP_WebViewProcessInfo *processInfo in webViewProcesses) { totalMemoryUse += processInfo.processMemoryUse; } [resultString appendString: TPILocalizedString(@"BasicLanguage[a5u-m1]", webViewProcesses.count, [TPI_SP_SysInfo formattedDiskSize:totalMemoryUse])]; return [resultString copy]; } + (NSString *)applicationRuntimeStatistics { NSTimeInterval runtime = [TPCApplicationInfo timeIntervalSinceApplicationInstall]; NSTimeInterval birthday = [NSDate timeIntervalSinceNow:[TPCApplicationInfo applicationBirthday]]; if (runtime > birthday) { runtime = birthday; } return TPILocalizedString(@"BasicLanguage[6fn-xh]", TXFormattedNumber([TPCApplicationInfo applicationRunCount]), TXHumanReadableTimeInterval(runtime, NO, 0)); } + (NSString *)systemDiskspaceInformation { NSMutableString *resultString = [NSMutableString string]; NSArray *volumeAttributes = @[NSURLVolumeNameKey, NSURLVolumeTotalCapacityKey, NSURLVolumeAvailableCapacityKey]; NSArray *volumes = [RZFileManager() mountedVolumeURLsIncludingResourceValuesForKeys:volumeAttributes options:NSVolumeEnumerationSkipHiddenVolumes]; [volumes enumerateObjectsUsingBlock:^(NSURL *volume, NSUInteger index, BOOL *stop) { NSString *volumeName = [volume resourceValueForKey:NSURLVolumeNameKey]; uint64_t totalSpace = [[volume resourceValueForKey:NSURLVolumeTotalCapacityKey] longLongValue]; uint64_t freeSpace = [[volume resourceValueForKey:NSURLVolumeAvailableCapacityKey] longLongValue]; if (index == 0) { [resultString appendString:TPILocalizedString(@"BasicLanguage[bvr-wz]", volumeName, [TPI_SP_SysInfo formattedDiskSize:totalSpace], [TPI_SP_SysInfo formattedDiskSize:freeSpace])]; } else { [resultString appendString:TPILocalizedString(@"BasicLanguage[lct-7h]", volumeName, [TPI_SP_SysInfo formattedDiskSize:totalSpace], [TPI_SP_SysInfo formattedDiskSize:freeSpace])]; } }]; if (resultString.length == 0) { return TPILocalizedString(@"BasicLanguage[ler-a5]"); } else { return TPILocalizedString(@"BasicLanguage[n6i-xd]", resultString); } } + (NSString *)systemDisplayInformation { NSMutableString *resultString = [NSMutableString string]; NSArray *screens = [NSScreen screens]; [screens enumerateObjectsUsingBlock:^(NSScreen *screen, NSUInteger index, BOOL *stop) { NSInteger screenNumber = (index + 1); NSString *refreshRate = [TPI_SP_SysInfo refreshRateForScreen:screen]; NSString *localization = nil; if (screenNumber == 1) { if (refreshRate == nil) { localization = @"BasicLanguage[441-8c]"; } else { localization = @"BasicLanguage[vvt-zq]"; } } else { if (refreshRate == nil) { localization = @"BasicLanguage[dd1-rp]"; } else { localization = @"BasicLanguage[fys-ft]"; } } if (refreshRate == nil) { refreshRate = @""; } [resultString appendString: TPILocalizedString(localization, screenNumber, screen.screenResolutionString, refreshRate)]; }]; return [resultString copy]; } + (NSString *)systemInformation { BOOL showCPUModel = ([RZUserDefaults() boolForKey:@"System Profiler Extension -> Feature Disabled -> CPU Model"] == NO); #if TARGET_CPU_ARM64 BOOL showGPUModel = NO; #elif TARGET_CPU_X86_64 BOOL showGPUModel = ([RZUserDefaults() boolForKey:@"System Profiler Extension -> Feature Disabled -> GPU Model"] == NO); #endif BOOL showDiskInfo = ([RZUserDefaults() boolForKey:@"System Profiler Extension -> Feature Disabled -> Disk Information"] == NO); BOOL showMemory = ([RZUserDefaults() boolForKey:@"System Profiler Extension -> Feature Disabled -> Memory Information"] == NO); BOOL showOperatingSystem = ([RZUserDefaults() boolForKey:@"System Profiler Extension -> Feature Disabled -> OS Version"] == NO); BOOL showScreenResolution = ([RZUserDefaults() boolForKey:@"System Profiler Extension -> Feature Disabled -> Screen Resolution"] == NO); BOOL showUptime = ([RZUserDefaults() boolForKey:@"System Profiler Extension -> Feature Disabled -> System Uptime"] == NO); NSMutableString *resultString = [NSMutableString string]; [resultString appendString:TPILocalizedString(@"BasicLanguage[lxj-ha]")]; NSString *modelIdentifier = [TPI_SP_SysInfo modelIdentifier]; if (modelIdentifier.length > 0) { NSString *modelsDictionaryPath = [TPIBundleFromClass() pathForResource:@"MacintoshModels" ofType:@"plist"]; NSDictionary *modelsDictionary = [NSDictionary dictionaryWithContentsOfFile:modelsDictionaryPath]; NSString *modelTitle = nil; if ([modelIdentifier hasPrefix:@"VMware"]) { modelTitle = modelsDictionary[@"VMware"]; } else if ([modelIdentifier hasPrefix:@"Parallels"]) { modelTitle = modelsDictionary[@"Parallels"]; } else { modelTitle = modelsDictionary[modelIdentifier]; } TEXTUAL_IGNORE_DEPRECATION_BEGIN if (modelTitle == nil) { modelTitle = [XRSystemInformation systemModelName]; } TEXTUAL_IGNORE_DEPRECATION_END [resultString appendString: TPILocalizedString(@"BasicLanguage[7g5-pf]", modelTitle)]; } if (showCPUModel) { NSString *_cpu_model = [TPI_SP_SysInfo processor]; NSUInteger _cpu_count_p = [TPI_SP_SysInfo processorPhysicalCoreCount]; #if TARGET_CPU_ARM64 if (_cpu_model.length > 0) { [resultString appendString: TPILocalizedString(@"BasicLanguage[ifk-s5]", _cpu_model, _cpu_count_p)]; } #elif TARGET_CPU_X86_64 NSString *_cpu_speed = [TPI_SP_SysInfo processorClockSpeed]; NSUInteger _cpu_count_v = [TPI_SP_SysInfo processorVirtualCoreCount]; _cpu_model = [XRRegularExpression string:_cpu_model replacedByRegex:@"(\\s*@.*)|CPU|\\(R\\)|\\(TM\\)" withString:@" "]; _cpu_model = [XRRegularExpression string:_cpu_model replacedByRegex:@"\\s+" withString:@" "]; _cpu_model = _cpu_model.trim; if (_cpu_model.length > 0 && _cpu_speed.length > 0) { [resultString appendString: TPILocalizedString(@"BasicLanguage[mnc-vx]", _cpu_model, _cpu_count_v, _cpu_count_p, _cpu_speed)]; } #endif } if (showMemory) { [resultString appendString: TPILocalizedString(@"BasicLanguage[1am-io]", [TPI_SP_SysInfo formattedTotalMemorySize])]; } if (showUptime) { [resultString appendString: TPILocalizedString(@"BasicLanguage[xb6-bh]", TXHumanReadableTimeInterval([TPI_SP_SysInfo systemUptime], YES, 0))]; } if (showDiskInfo) { NSString *_disk_info = [TPI_SP_SysInfo formattedLocalVolumeDiskUsage]; if (_disk_info != nil) { [resultString appendString: TPILocalizedString(@"BasicLanguage[yrc-6l]", _disk_info)]; } } if (showGPUModel) { NSString *_gpu_model = [TPI_SP_SysInfo formattedGraphicsCardInformation]; if (_gpu_model != nil) { [resultString appendString: TPILocalizedString(@"BasicLanguage[q5v-uq]", _gpu_model)]; } } if (showScreenResolution) { NSScreen *mainScreen = RZMainScreen(); NSString *refreshRate = [TPI_SP_SysInfo refreshRateForScreen:mainScreen]; if (refreshRate == nil) { [resultString appendString: TPILocalizedString(@"BasicLanguage[22o-rg]", mainScreen.screenResolutionString)]; } else { [resultString appendString: TPILocalizedString(@"BasicLanguage[b7c-qd]", mainScreen.screenResolutionString, refreshRate)]; } } if (showOperatingSystem) { [resultString appendString: TPILocalizedString(@"BasicLanguage[g41-p7]", [XRSystemInformation systemOperatingSystemName], [XRSystemInformation systemStandardVersion], [XRSystemInformation systemBuildVersion])]; } if ([resultString hasSuffix:@" \002•\002"]) { [resultString deleteCharactersInRange:NSMakeRange((resultString.length - 4), 4)]; } return [resultString copy]; } + (NSString *)systemMemoryInformation { uint64_t totalMemory = [TPI_SP_SysInfo totalMemorySize]; uint64_t freeMemory = [TPI_SP_SysInfo freeMemorySize]; uint64_t usedMemory = (totalMemory - freeMemory); long double memoryUsedPercent = (((long double)usedMemory / (long double)totalMemory) * 100.0); NSMutableString *resultString = [NSMutableString string]; /* ======================================== */ [resultString appendFormat:@"%c04", 0x03]; NSUInteger leftCount = (memoryUsedPercent / 10); for (NSUInteger i = 0; i <= leftCount; i++) { [resultString appendString:@"❙"]; } /* ======================================== */ [resultString appendFormat:@"%c|%c03", 0x03, 0x03]; /* ======================================== */ NSUInteger rightCount = (10 - leftCount); for (NSUInteger i = 0; i <= rightCount; i++) { [resultString appendString:@"❙"]; } [resultString appendFormat:@"%c", 0x03]; /* ======================================== */ return TPILocalizedString(@"BasicLanguage[cfs-b1]", [TPI_SP_SysInfo formattedDiskSize:freeMemory], [TPI_SP_SysInfo formattedDiskSize:usedMemory], [TPI_SP_SysInfo formattedDiskSize:totalMemory], resultString); } + (NSString *)systemNetworkInformation { /* Based off the source code of "libtop.c" */ NSMutableString *resultString = [NSMutableString string]; struct ifaddrs *ifa_list = 0; if (getifaddrs(&ifa_list) == (-1)) { return TPILocalizedString(@"BasicLanguage[li1-vn]"); } NSUInteger objectIndex = 0; for (struct ifaddrs *ifa = ifa_list; ifa; ifa = ifa->ifa_next) { if ((AF_LINK == ifa->ifa_addr->sa_family) == NO) { continue; } else if ((ifa->ifa_flags & IFF_UP) == NO && (ifa->ifa_flags & IFF_RUNNING) == NO) { continue; } else if (ifa->ifa_data == 0) { continue; } if (strncmp(ifa->ifa_name, "lo", 2) == 0) { continue; } struct if_data *if_data = (struct if_data *)ifa->ifa_data; if (if_data->ifi_ibytes < 20000000 || if_data->ifi_obytes < 2000000) { continue; } if (objectIndex == 0) { [resultString appendString:TPILocalizedString(@"BasicLanguage[ca4-25]", @(ifa->ifa_name), [TPI_SP_SysInfo formattedDiskSize:if_data->ifi_ibytes], [TPI_SP_SysInfo formattedDiskSize:if_data->ifi_obytes])]; } else { [resultString appendString:TPILocalizedString(@"BasicLanguage[mjo-o0]", @(ifa->ifa_name), [TPI_SP_SysInfo formattedDiskSize:if_data->ifi_ibytes], [TPI_SP_SysInfo formattedDiskSize:if_data->ifi_obytes])]; } objectIndex += 1; } if (ifa_list) { freeifaddrs(ifa_list); } if (resultString.length == 0) { return TPILocalizedString(@"BasicLanguage[li1-vn]"); } else { return TPILocalizedString(@"BasicLanguage[9f8-ej]", resultString); } return resultString; } @end @implementation TPI_SP_SysInfo #pragma mark - #pragma mark Formatting/Processing + (NSString *)formattedDiskSize:(uint64_t)diskSize { return [NSByteCountFormatter stringFromByteCountWithPaddedDigits:diskSize]; } + (NSString *)formattedCPUFrequency:(double)frequency { if ((frequency / 1000000) >= 990) { return TPILocalizedString(@"BasicLanguage[3iu-k8]", ((frequency / 100000000.0) / 10.0)); } else { return TPILocalizedString(@"BasicLanguage[jw7-sg]", frequency); } } + (NSString *)formattedTotalMemorySize { return [self formattedDiskSize:[self totalMemorySize]]; } + (nullable NSString *)formattedLocalVolumeDiskUsage { NSDictionary *diskInfo = [RZFileManager() attributesOfFileSystemForPath:@"/" error:nil]; if (diskInfo == nil) { return nil; } uint64_t totalSpace = [diskInfo longLongForKey:NSFileSystemSize]; return [self formattedDiskSize:totalSpace]; } + (nullable NSString *)formattedGraphicsCardInformation { CFMutableDictionaryRef pciDevices = IOServiceMatching("IOPCIDevice"); io_iterator_t entryIterator; if (IOServiceGetMatchingServices(kIOMasterPortDefault, pciDevices, &entryIterator) != kIOReturnSuccess) { return nil; } NSMutableArray *gpuModels = [NSMutableArray new]; io_iterator_t serviceObject; while ((serviceObject = IOIteratorNext(entryIterator))) { CFMutableDictionaryRef serviceDictionary; kern_return_t status = IORegistryEntryCreateCFProperties(serviceObject, &serviceDictionary, kCFAllocatorDefault, kNilOptions); if (status != kIOReturnSuccess) { IOObjectRelease(serviceObject); continue; } BOOL cleanResult = YES; const void *classCode = CFDictionaryGetValue(serviceDictionary, @"class-code"); if (classCode == NULL) { cleanResult = NO; } if (CFGetTypeID(classCode) != CFDataGetTypeID()) { cleanResult = NO; } else if (CFDataGetLength(classCode) == 0) { cleanResult = NO; } else if (*(UInt32 *)CFDataGetBytePtr(classCode) != 0x30000) { cleanResult = NO; } const void *model = CFDictionaryGetValue(serviceDictionary, @"model"); if (model == NULL) { cleanResult = NO; } else if (CFGetTypeID(model) != CFDataGetTypeID()) { cleanResult = NO; } else if (CFDataGetLength(model) == 0) { cleanResult = NO; } if (cleanResult) { NSString *modelString = [NSString stringWithData:(__bridge NSData *)(CFDataRef)model encoding:NSASCIIStringEncoding]; modelString = [modelString stringByReplacingOccurrencesOfString:@"\0" withString:@""]; [gpuModels addObject:modelString]; } CFRelease(serviceDictionary); } // ---- // NSMutableString *resultString = [NSMutableString string]; [gpuModels enumerateObjectsUsingBlock:^(NSString *gpuModel, NSUInteger index, BOOL *stop) { if (index == 0) { [resultString appendString:TPILocalizedString(@"BasicLanguage[8nu-89]", gpuModel)]; } else { [resultString appendString:TPILocalizedString(@"BasicLanguage[cmk-ws]", gpuModel)]; } }]; return [resultString copy]; } + (NSString *)descriptionForSidebarAppearance { if (mainWindow().usingDarkAppearance) { return TPILocalizedString(@"BasicLanguage[243-yt]"); // Dark } return TPILocalizedString(@"BasicLanguage[890-au]"); // Light } + (NSString *)descriptionForThemeAppearance { TPCThemeAppearanceType appearance = themeController().theme.appearance; return [self descriptionForThemeAppearance:appearance]; } + (NSString *)descriptionForThemeAppearance:(TPCThemeAppearanceType)appearance { if (appearance == TPCThemeAppearanceTypeDefault) { TXAppearance *appAppearance = [TXSharedApplication sharedAppearance]; if (appAppearance.properties.isDarkAppearance) { appearance = TPCThemeAppearanceTypeDark; } else { appearance = TPCThemeAppearanceTypeLight; } } if (appearance == TPCThemeAppearanceTypeDark) { return TPILocalizedString(@"BasicLanguage[243-yt]"); // Dark } return TPILocalizedString(@"BasicLanguage[890-au]"); // Light } #pragma mark - #pragma mark System Information + (NSTimeInterval)systemUptime { struct timeval bootTime; size_t bootTimeSize = sizeof(bootTime); if (sysctlbyname("kern.boottime", &bootTime, &bootTimeSize, NULL, 0) != 0) { bootTime.tv_sec = 0; } return [NSDate timeIntervalSinceNow:bootTime.tv_sec]; } + (NSTimeInterval)applicationUptime { return [TPCApplicationInfo timeIntervalSinceApplicationLaunch]; } + (nullable NSString *)processor { char buffer[256]; size_t bufferSize = sizeof(buffer); if (sysctlbyname("machdep.cpu.brand_string", buffer, &bufferSize, NULL, 0) != 0) { return nil; } buffer[(bufferSize - 1)] = 0; return @(buffer); } + (nullable NSString *)modelIdentifier { char buffer[256]; size_t bufferSize = sizeof(buffer); if (sysctlbyname("hw.model", buffer, &bufferSize, NULL, 0) != 0) { return nil; } buffer[(bufferSize - 1)] = 0; return @(buffer); } + (NSUInteger)processorPhysicalCoreCount { u_int64_t coreCount = 0L; size_t coreCountSize = sizeof(coreCount); if (sysctlbyname("hw.physicalcpu", &coreCount, &coreCountSize, NULL, 0) != 0) { return 0; } return coreCount; } + (NSUInteger)processorVirtualCoreCount { u_int64_t coreCount = 0L; size_t coreCountSize = sizeof(coreCount); if (sysctlbyname("hw.logicalcpu", &coreCount, &coreCountSize, NULL, 0) != 0) { return 0; } return coreCount; } + (nullable NSString *)processorClockSpeed { u_int64_t clockSpeed = 0L; size_t clockSpeedSize = sizeof(clockSpeed); if (sysctlbyname("hw.cpufrequency", &clockSpeed, &clockSpeedSize, NULL, 0) != 0) { return nil; } return [self formattedCPUFrequency:clockSpeed]; } + (uint64_t)freeMemorySize { vm_size_t page_size; host_page_size(mach_host_self(), &page_size); vm_statistics_data_t host_info_out; mach_msg_type_number_t host_info_outCnt = (sizeof(vm_statistics_data_t) / sizeof(natural_t)); if (host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&host_info_out, &host_info_outCnt) != KERN_SUCCESS) { return 0; } return ((host_info_out.inactive_count + host_info_out.free_count) * page_size); } + (uint64_t)totalMemorySize { uint64_t memoryTotal = 0L; size_t memoryTotalSize = sizeof(memoryTotal); if (sysctlbyname("hw.memsize", &memoryTotal, &memoryTotalSize, NULL, 0) != 0) { return 0; } return (memoryTotal / _systemMemoryDivisor); } + (uint64_t)applicationMemoryInformation { pid_t processIdentifier = (pid_t)[NSProcessInfo processInfo].processIdentifier; return [TPI_SP_SysInfo memoryUseForProcess:processIdentifier]; } + (uint64_t)memoryUseForProcess:(pid_t)processIdentifier { if (processIdentifier == 0) { return 0; } int processLookupResult = 0; struct proc_regioninfo processRegionInfo; uint64_t processAddress = 0; uint64_t memoryUse = 0; int memoryPageSize = getpagesize(); do { processLookupResult = proc_pidinfo(processIdentifier, PROC_PIDREGIONINFO, processAddress, &processRegionInfo, PROC_PIDREGIONINFO_SIZE); processAddress = (processRegionInfo.pri_address + processRegionInfo.pri_size); if (processRegionInfo.pri_share_mode == SM_PRIVATE) { memoryUse += (processRegionInfo.pri_private_pages_resident * memoryPageSize); } } while (processLookupResult > 0); return memoryUse; } + (NSArray *)webViewProcessIdentifiers { /* Create a dictionary with key as identifier and value as an array of views managed by the process. */ NSMutableDictionary *webViewProcesses = [NSMutableDictionary dictionary]; void (^_addEntry)(IRCTreeItem *) = ^void (IRCTreeItem *treeItem) { pid_t processIdentifier = [TPI_SP_SysInfo webViewProcessIdentifierForTreeItem:treeItem]; if (processIdentifier == 0) { return; } NSNumber *processIdentifierObj = @(processIdentifier); NSMutableArray *viewArray = webViewProcesses[processIdentifierObj]; if (viewArray == nil) { viewArray = [NSMutableArray array]; webViewProcesses[processIdentifierObj] = viewArray; } if (treeItem.isClient) { return; } [viewArray addObject:treeItem.name]; }; for (IRCClient *u in worldController().clientList) { _addEntry(u); for (IRCChannel *c in u.channelList) { _addEntry(c); } } /* Create array of TPI_SP_WebViewProcessInfo objects */ NSMutableArray *webViewProcessObjects = [NSMutableArray arrayWithCapacity:webViewProcesses.count]; [webViewProcesses enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop) { /* Object values */ pid_t processIdentifier = [key intValue]; uint64_t processMemoryUse = [TPI_SP_SysInfo memoryUseForProcess:processIdentifier]; NSArray *processViewNames = [object sortedArrayUsingSelector:@selector(compare:)]; /* Set values */ TPI_SP_WebViewProcessInfo *processInfoObject = [TPI_SP_WebViewProcessInfo new]; processInfoObject.processIdentifier = processIdentifier; processInfoObject.processMemoryUse = processMemoryUse; processInfoObject.processViewNames = processViewNames; /* Add object */ [webViewProcessObjects addObject:processInfoObject]; }]; /* Sort objects based on memory use (highest to lowest) */ [webViewProcessObjects sortUsingComparator:^NSComparisonResult(id object1, id object2) { uint64_t processMemoryUse1 = [object1 processMemoryUse]; uint64_t processMemoryUse2 = [object2 processMemoryUse]; if (processMemoryUse1 > processMemoryUse2) { return NSOrderedAscending; } else if (processMemoryUse1 < processMemoryUse2) { return NSOrderedDescending; } return NSOrderedSame; }]; /* Return a copy of mutable array */ return [webViewProcessObjects copy]; } + (pid_t)webViewProcessIdentifierForTreeItem:(IRCTreeItem *)treeItem { TVCLogView *backingView = treeItem.viewController.backingView; id webView = backingView.webView; if ([webView respondsToSelector:@selector(_webProcessIdentifier)]) { return [webView _webProcessIdentifier]; } return 0; } + (nullable NSString *)refreshRateForScreen:(NSScreen *)screen { NSParameterAssert(screen != nil); CGFloat refreshRate = screen.screenRefreshRate; /* Only return a formatted refresh rate if not 60. Everyone has 60. That's not interesting info. */ /* Edited June 2024: Some displays are actually 59. It annoyed me having this information displayed. The values I picked below for excluding this information were picked arbitrarily and do not reflect any real world testing. */ // if (fabs(refreshRate) == 60.0) { if (refreshRate > 58.5 && refreshRate < 61.5) { return nil; } return TPILocalizedString(@"BasicLanguage[zpt-sx]", refreshRate); } @end #pragma mark - @implementation TPI_SP_WebViewProcessInfo @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/System Profiler/Resources/Language Files/en.lproj/BasicLanguage.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2012 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Preferences pane name. */ "dff-13" = "System Information"; /* Screen refresh rate - shared by /sysinfo/ and /screens/ */ "zpt-sx" = "%.0f Hz"; /* /sysinfo/ command. */ "lxj-ha" = "System Information:"; "7g5-pf" = " \002Model:\002 %@ \002•\002"; "mnc-vx" = " \002CPU:\002 %1$@ (%2$lu Threads, %3$lu Cores) @ %4$@ \002•\002"; "ifk-s5" = " \002CPU:\002 %1$@ (%2$lu Cores) \002•\002"; "1am-io" = " \002Memory:\002 %@ \002•\002"; "xb6-bh" = " \002Uptime:\002 %@ \002•\002"; "yrc-6l" = " \002Space:\002 %@ \002•\002"; "q5v-uq" = " \002Graphics:\002 %@ \002•\002"; "22o-rg" = " \002Display:\002 %1$@ \002•\002"; "b7c-qd" = " \002Display:\002 %1$@ at %2$@ \002•\002"; "g41-p7" = " \002OS:\002 %1$@ (Version %2$@, Build %3$@)"; "8nu-89" = "%@"; "cmk-ws" = ", %@"; "3iu-k8" = "%.2f GHz"; "jw7-sg" = "%.2f MHz"; // really? /* /memory/ command. */ "scn-br" = "Memory use of Textual: %1$@ • Visible lines: %2$@"; "ioy-qc" = "\002Top Process (WebKit):\002 Process #%1$i, which is assigned to %2$@, is using %3$@ of memory"; "a00-8n" = "\002Top Process (WebKit):\002 Process #%1$i, which is assigned to %2$@; is using %3$@ of memory"; "a5u-m1" = "\002All Processes (WebKit):\002 Process count: %1$lu • Memory use: %2$@"; /* /diskspace/ command. */ "n6i-xd" = "\002Mounted Drives:\002 %@"; "bvr-wz" = "%1$@: Total: %2$@; Free: %3$@"; "lct-7h" = " — %1$@: Total: %2$@; Free: %3$@"; "ler-a5" = "Error: Unable to find any mounted drives."; // what are we running on? /* /netstats/ command. */ "9f8-ej" = "\002Network Traffic:\002%@"; "ca4-25" = " [%1$@]: %2$@ in, %3$@ out"; "mjo-o0" = " — [%1$@]: %2$@ in, %3$@ out"; "li1-vn" = "Error: Unable to locate any network statistics."; /* /style/ command. */ "z37-85" = "\002Current Style:\002 %1$@ (%2$@) \002•\002 %3$@"; "614-dj" = "\002Appearance:\002 %@"; "843-z4" = "\002Chat Appearance:\002 %1$@ \002•\002 \002Sidebar Appearance:\002 %2$@"; "890-au" = "Light"; "243-yt" = "Dark"; /* /screens/ command. */ "vvt-zq" = "\002Display Resolution:\002 #%1$lu: %2$@ at %3$@"; "441-8c" = "\002Display Resolution:\002 #%1$lu: %2$@"; "fys-ft" = "; #%1$lu: %2$@ at %3$@"; "dd1-rp" = "; #%1$lu: %2$@"; /* Misc., one line commands. */ "v03-jx" = "System Uptime: %1$@ \002•\002 Textual Uptime: %2$@"; "cfs-b1" = "\002System Memory:\002 Free: %1$@; Used: %2$@; Total: %3$@; — [%4$@]"; "6fn-xh" = "Textual has been opened \002%1$@\002 times with a total runtime of %2$@"; "rua-9r" = "Textual has sent \002%1$@\002 commands since startup with a total of \002%2$@\002 commands received. The last command received on this connection occurred %3$@ ago. This equals roughly \002%4$@ in\002 and \002%5$@ out\002 worth of bandwidth."; ================================================ FILE: Sources/Plugins/System Profiler/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.1 NSHumanReadableCopyright Copyright © 2012 - 2020 Codeux Software, LLC. All rights reserved. MinimumTextualVersion 7.2.4 NSPrincipalClass TPISystemProfiler ================================================ FILE: Sources/Plugins/System Profiler/Resources/Property Lists/MacintoshModels.plist ================================================ Mac13,1 Mac Studio (2022) Mac13,2 Mac Studio (2022) Mac14,2 MacBook Air (13-inch, 2022) Mac14,3 Mac mini (2023) Mac14,5 MacBook Pro (14-inch, Early 2023) Mac14,6 MacBook Pro (16-inch, Early 2023) Mac14,7 MacBook Pro (13-inch, 2022) Mac14,8 Mac Pro (2023) Mac14,9 MacBook Pro (14-inch, Early 2023) Mac14,10 MacBook Pro (16-inch, Early 2023) Mac14,12 Mac mini (2023) Mac14,13 Mac Studio (2023) Mac14,14 Mac Studio (2023) Mac14,15 MacBook Air (15-inch, 2023) Mac15,3 MacBook Pro (14-inch, Late 2023) Mac15,4 iMac (24-inch, 2023) Mac15,5 iMac (24-inch, 2023) Mac15,6 MacBook Pro (14-inch, Late 2023) Mac15,7 MacBook Pro (16-inch, Late 2023) Mac15,8 MacBook Pro (14-inch, Late 2023) Mac15,9 MacBook Pro (16-inch, Late 2023) Mac15,10 MacBook Pro (14-inch, Late 2023) Mac15,11 MacBook Pro (16-inch, Late 2023) Mac15,12 MacBook Air (13-inch, 2024) Mac15,13 MacBook Air (15-inch, 2024) Mac15,14 Mac Studio (2025) Mac16,1 MacBook Pro (14-inch, 2024) Mac16,2 iMac (24-inch, 2024) Mac16,3 iMac (24-inch, 2024) Mac16,5 MacBook Pro (16-inch, 2024) Mac16,6 MacBook Pro (14-inch, 2024) Mac16,7 MacBook Pro (16-inch, 2024) Mac16,8 MacBook Pro (14-inch, 2024) Mac16,9 Mac Studio (2025) Mac16,10 Mac mini (2024) Mac16,11 Mac mini (2024) Mac16,12 MacBook Air (13-inch, 2025) Mac16,13 MacBook Air (15-inch, 2025) MacBook6,1 MacBook (13-inch, Late 2009) MacBook7,1 MacBook (13-inch, 2010) MacBook8,1 MacBook (12-inch, 2015) MacBook9,1 MacBook (12-inch, Early 2016) MacBook10,1 MacBook (12-inch, 2017) MacBookAir3,1 MacBook Air (11-inch, 2010) MacBookAir3,2 MacBook Air (13-inch, 2010) MacBookAir4,1 MacBook Air (11-inch, 2011) MacBookAir4,2 MacBook Air (13-inch, 2011) MacBookAir5,1 MacBook Air (11-inch, 2012) MacBookAir5,2 MacBook Air (13-inch, 2012) MacBookAir6,1 MacBook Air (11-inch, 2013) MacBookAir6,2 MacBook Air (13-inch, 2013) MacBookAir7,1 MacBook Air (11-inch, 2015) MacBookAir7,2 MacBook Air (13-inch, 2015) MacBookAir8,1 MacBook Air (13-inch, 2018) MacBookAir9,1 MacBook Air (13-inch, Early 2020) MacBookAir10,1 MacBook Air (13-inch, Late 2020) MacBookPro6,1 MacBook Pro (17-inch, Mid-2010) MacBookPro6,2 MacBook Pro (15-inch, Mid-2010) MacBookPro7,1 MacBook Pro (13-inch, Mid-2010) MacBookPro8,1 MacBook Pro (13-inch, 2011) MacBookPro8,2 MacBook Pro (15-inch, 2011) MacBookPro8,3 MacBook Pro (17-inch, 2011) MacBookPro9,2 MacBook Pro (13-inch, 2012) MacBookPro9,1 MacBook Pro (15-inch, 2012) MacBookPro10,1 MacBook Pro (15-inch, 2012) MacBookPro10,2 MacBook Pro (13-inch, 2012) MacBookPro11,1 MacBook Pro (13-inch, 2013) MacBookPro11,2 MacBook Pro (15-inch, 2013) MacBookPro11,3 MacBook Pro (15-inch, 2013) MacBookPro11,4 MacBook Pro (15-inch, Mid-2015) MacBookPro11,5 MacBook Pro (15-inch, Mid-2015) MacBookPro12,1 MacBook Pro (13-inch, Early 2015) MacBookPro13,1 MacBook Pro (13-inch, 2016) MacBookPro13,2 MacBook Pro (13-inch, 2016) MacBookPro13,3 MacBook Pro (15-inch, 2016) MacBookPro14,1 MacBook Pro (13-inch, 2017) MacBookPro14,2 MacBook Pro (13-inch, 2017) MacBookPro14,3 MacBook Pro (15-inch, 2017) MacBookPro15,1 MacBook Pro (15-inch, 2018) MacBookPro15,2 MacBook Pro (13-inch, 2018) MacBookPro15,3 MacBook Pro (15-inch, 2019) MacBookPro16,1 MacBook Pro (16-inch, 2019) MacBookPro16,2 MacBook Pro (13-inch, 2020) MacBookPro16,3 MacBook Pro (13-inch, 2020) MacBookPro17,1 MacBook Pro (13-inch, 2020) MacBookPro18,1 MacBook Pro (16-inch, 2021) MacBookPro18,2 MacBook Pro (16-inch, 2021) MacBookPro18,3 MacBook Pro (14-inch, 2021) MacBookPro18,4 MacBook Pro (14-inch, 2021) MacPro5,1 Mac Pro (2010) MacPro6,1 Mac Pro (2013) MacPro7,1 Mac Pro (2019) Macmini4,1 Mac Mini (2010) Macmini5,1 Mac Mini (2011) Macmini5,2 Mac Mini (2011) Macmini5,3 Mac Mini Server (2011) Macmini6,1 Mac Mini (2012) Macmini6,2 Mac Mini (2012) Macmini7,1 Mac Mini (2014) Macmini8,1 Mac Mini (2018) Macmini9,1 Mac Mini (2020) iMac10,1 iMac (Late 2009) iMac11,1 iMac (Late 2009) iMac11,2 iMac (21.5-inch, 2010) iMac11,3 iMac (27-inch, 2010) iMac12,1 iMac (21.5-inch, 2011) iMac12,2 iMac (27-inch, 2011) iMac13,1 iMac (21.5-inch, 2012) iMac13,2 iMac (27-inch, 2012) iMac14,1 iMac (21.5-inch, 2013) iMac14,2 iMac (27-inch, 2013) iMac14,3 iMac (21.5-inch, 2013) iMac14,4 iMac (21.5-inch, 2014) iMac15,1 iMac (27-inch, 2014) iMac16,1 iMac (21.5-Inch, 2015) iMac16,2 iMac (21.5-Inch, 2015) iMac17,1 iMac (27-inch, 2015) iMac18,1 iMac (21.5-Inch, 2017) iMac18,2 iMac (21.5-Inch, 2017) iMac18,3 iMac (27-inch, 2017) iMac19,1 iMac (27-inch, 2019) iMac19,2 iMac (21.5-inch, 2019) iMac20,1 iMac (27-inch, 2020) iMac20,2 iMac (27-inch, 2020) iMac21,1 iMac (24-inch, 2021) iMac21,2 iMac (24-inch, 2021) iMacPro1,1 iMac (27-inch, 2017) Parallels Parallels Desktop Virtual Machine VMware VMware Fusion Virtual Machine ================================================ FILE: Sources/Plugins/System Profiler/Resources/User Interface/en.lproj/TPISystemProfiler.xib ================================================ ================================================ FILE: Sources/Plugins/System Profiler/System Profiler Extension.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C06DE7A20EC45D10055D09A /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C06DE7920EC45D10055D09A /* IOKit.framework */; }; 4C36AAD420F27C9E007CA939 /* TPISystemProfiler.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAD220F27C9E007CA939 /* TPISystemProfiler.xib */; }; 4C36AAD920F27CC5007CA939 /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAD720F27CC5007CA939 /* BasicLanguage.strings */; }; 4C46A08920EC6ABD00094EA4 /* MacintoshModels.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4C46A08220EC6ABD00094EA4 /* MacintoshModels.plist */; }; 4C46A09520EC6AD700094EA4 /* TPI_SP_SysInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A08F20EC6AD700094EA4 /* TPI_SP_SysInfo.m */; }; 4C46A09720EC6AD700094EA4 /* TPISystemProfiler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A09120EC6AD700094EA4 /* TPISystemProfiler.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C06DE7920EC45D10055D09A /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 4C36AAD320F27C9E007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TPISystemProfiler.xib; sourceTree = ""; }; 4C36AAD820F27CC5007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BasicLanguage.strings; sourceTree = ""; }; 4C46A08220EC6ABD00094EA4 /* MacintoshModels.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = MacintoshModels.plist; sourceTree = ""; }; 4C46A08320EC6ABD00094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C46A08E20EC6AD700094EA4 /* TPISystemProfiler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPISystemProfiler.h; sourceTree = ""; }; 4C46A08F20EC6AD700094EA4 /* TPI_SP_SysInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_SP_SysInfo.m; sourceTree = ""; }; 4C46A09120EC6AD700094EA4 /* TPISystemProfiler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPISystemProfiler.m; sourceTree = ""; }; 4C46A09220EC6AD700094EA4 /* SystemProfiler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SystemProfiler.h; sourceTree = ""; }; 4C46A09420EC6AD700094EA4 /* TPI_SP_SysInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_SP_SysInfo.h; sourceTree = ""; }; 4CE1439920EBF551000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1439A20EBF551000E01C9 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4CE1439E20EBF551000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE143AC20EBF551000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE143AD20EBF551000E01C9 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4CE143AE20EBF551000E01C9 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4CE143B020EBF551000E01C9 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4CE143B320EBF551000E01C9 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4CE143C120EBF551000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE143C220EBF551000E01C9 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4CE143C620EBF551000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 8D576316048677EA00EA77CD /* System Info.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "System Info.bundle"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4C06DE7A20EC45D10055D09A /* IOKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* BlowfishCommandLine */ = { isa = PBXGroup; children = ( 4C46A08D20EC6AD700094EA4 /* Classes */, 089C167CFE841241C02AAC07 /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = BlowfishCommandLine; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( 4C06DE7920EC45D10055D09A /* IOKit.framework */, ); name = Frameworks; sourceTree = ""; }; 089C167CFE841241C02AAC07 /* Resources */ = { isa = PBXGroup; children = ( 4C69188E16E3276200BDE3A8 /* Configurations */, 4C46A08720EC6ABD00094EA4 /* Language Files */, 4C46A08120EC6ABD00094EA4 /* Property Lists */, 4C46A08420EC6ABD00094EA4 /* User Interface */, ); path = Resources; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* System Info.bundle */, ); name = Products; sourceTree = ""; }; 4C46A08120EC6ABD00094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A08220EC6ABD00094EA4 /* MacintoshModels.plist */, 4C46A08320EC6ABD00094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A08420EC6ABD00094EA4 /* User Interface */ = { isa = PBXGroup; children = ( 4C36AAD220F27C9E007CA939 /* TPISystemProfiler.xib */, ); path = "User Interface"; sourceTree = ""; }; 4C46A08720EC6ABD00094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C36AAD720F27CC5007CA939 /* BasicLanguage.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C46A08D20EC6AD700094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A09220EC6AD700094EA4 /* SystemProfiler.h */, 4C46A09420EC6AD700094EA4 /* TPI_SP_SysInfo.h */, 4C46A08F20EC6AD700094EA4 /* TPI_SP_SysInfo.m */, 4C46A08E20EC6AD700094EA4 /* TPISystemProfiler.h */, 4C46A09120EC6AD700094EA4 /* TPISystemProfiler.m */, ); path = Classes; sourceTree = ""; }; 4C69188E16E3276200BDE3A8 /* Configurations */ = { isa = PBXGroup; children = ( 4CE1439520EBF551000E01C9 /* Build */, ); name = Configurations; path = ../../../Configurations; sourceTree = SOURCE_ROOT; }; 4CE1439520EBF551000E01C9 /* Build */ = { isa = PBXGroup; children = ( 4CE143A920EBF551000E01C9 /* Common */, 4CE143BE20EBF551000E01C9 /* Debug */, 4CE1439620EBF551000E01C9 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4CE1439620EBF551000E01C9 /* Standard Release */ = { isa = PBXGroup; children = ( 4CE1439E20EBF551000E01C9 /* Enabled Features.xcconfig */, 4CE1439A20EBF551000E01C9 /* Textual Extensions.xcconfig */, 4CE1439920EBF551000E01C9 /* Textual.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4CE143A920EBF551000E01C9 /* Common */ = { isa = PBXGroup; children = ( 4CE143B320EBF551000E01C9 /* Foundation Debug.xcconfig */, 4CE143B020EBF551000E01C9 /* Foundation.xcconfig */, 4CE143AD20EBF551000E01C9 /* Preserve Symbols.xcconfig */, 4CE143AE20EBF551000E01C9 /* Textual Extensions.xcconfig */, 4CE143AC20EBF551000E01C9 /* Textual.xcconfig */, ); path = Common; sourceTree = ""; }; 4CE143BE20EBF551000E01C9 /* Debug */ = { isa = PBXGroup; children = ( 4CE143C620EBF551000E01C9 /* Enabled Features.xcconfig */, 4CE143C220EBF551000E01C9 /* Textual Extensions.xcconfig */, 4CE143C120EBF551000E01C9 /* Textual.xcconfig */, ); path = Debug; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* System Profiler Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "System Profiler Extension" */; buildPhases = ( 4CD69D80131E2BB500F4618E /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "System Profiler Extension"; productInstallPath = "$(HOME)/Library/Bundles"; productName = BlowfishCommandLine; productReference = 8D576316048677EA00EA77CD /* System Info.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; TargetAttributes = { 8D57630D048677EA00EA77CD = { DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "System Profiler Extension" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( en, Base, ); mainGroup = 089C166AFE841209C02AAC07 /* BlowfishCommandLine */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* System Profiler Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4CD69D80131E2BB500F4618E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A08920EC6ABD00094EA4 /* MacintoshModels.plist in Resources */, 4C36AAD420F27C9E007CA939 /* TPISystemProfiler.xib in Resources */, 4C36AAD920F27CC5007CA939 /* BasicLanguage.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A09520EC6AD700094EA4 /* TPI_SP_SysInfo.m in Sources */, 4C46A09720EC6AD700094EA4 /* TPISystemProfiler.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C36AAD220F27C9E007CA939 /* TPISystemProfiler.xib */ = { isa = PBXVariantGroup; children = ( 4C36AAD320F27C9E007CA939 /* en */, ); name = TPISystemProfiler.xib; sourceTree = ""; }; 4C36AAD720F27CC5007CA939 /* BasicLanguage.strings */ = { isa = PBXVariantGroup; children = ( 4C36AAD820F27CC5007CA939 /* en */, ); name = BasicLanguage.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1DEB911B08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-SystemProfiler"; PRODUCT_NAME = "System Info"; }; name = Debug; }; 1DEB911C08733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-SystemProfiler"; PRODUCT_NAME = "System Info"; }; name = Release; }; 1DEB911F08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE143C220EBF551000E01C9 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Debug; }; 1DEB912008733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE1439A20EBF551000E01C9 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4C95801E1FA41F6D00F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (App Store)"; }; 4C95801F1FA41F6D00F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-SystemProfiler"; PRODUCT_NAME = "System Info"; }; name = "Release (App Store)"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "System Profiler Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911B08733D790010E9CD /* Debug */, 1DEB911C08733D790010E9CD /* Release */, 4C95801F1FA41F6D00F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "System Profiler Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911F08733D790010E9CD /* Debug */, 1DEB912008733D790010E9CD /* Release */, 4C95801E1FA41F6D00F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Plugins/User Insights/Classes/TPIUserInsights.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2013 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" NS_ASSUME_NONNULL_BEGIN @interface TPIUserInsights : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/User Insights/Classes/TPIUserInsights.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2013 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPIUserInsights.h" NS_ASSUME_NONNULL_BEGIN @implementation TPIUserInsights #pragma mark - #pragma mark User Input - (void)userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString { XRPerformBlockAsynchronouslyOnMainQueue(^{ [self _userInputCommandInvokedOnClient:client commandString:commandString messageString:messageString]; }); } - (void)_userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString { IRCChannel *channel = mainWindow().selectedChannel; /* We can brag in private messages so add above if statement */ if ([commandString isEqualToString:@"BRAG"]) { [self bragInChannel:channel onClient:client]; return; } if (channel.isChannel == NO) { return; } messageString = messageString.trim; if ([commandString isEqualToString:@"CLONES"]) { [self findAllClonesInChannel:channel onClient:client]; } else if ([commandString isEqualToString:@"NAMEL"]) { [self buildListOfUsersInChannel:channel onClient:client parameters:messageString]; } else if ([commandString isEqualToString:@"FINDUSER"]) { [self findAllUsersMatchingString:messageString inChannel:channel onClient:client]; } } - (NSArray *)subscribedUserInputCommands { return @[@"clones", @"namel", @"finduser", @"brag"]; } - (void)buildListOfUsersInChannel:(IRCChannel *)channel onClient:(IRCClient *)client parameters:(NSString *)parameters { NSParameterAssert(channel != nil); NSParameterAssert(client != nil); NSParameterAssert(parameters != nil); NSArray *memberList = channel.memberList; if (memberList.count == 0) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[5dx-rs]", channel.name) inChannel:channel]; return; } /* Process parameters */ BOOL displayRank = NO; BOOL sortByRank = NO; if ([parameters hasPrefix:@"-"]) { NSString *flagsString = [parameters substringFromIndex:1]; NSArray *flags = flagsString.characterStringBuffer; displayRank = [flags containsObject:@"d"]; sortByRank = [flags containsObject:@"r"]; } /* -memberList returns a list sorted by rank by default. If we are not sorting by rank, then we have to first get the member list then sort it another way. */ if (sortByRank == NO) { /* Sort user objects alphabetically by comparing nicknames */ memberList = [memberList sortedArrayUsingComparator:^NSComparisonResult(IRCChannelUser *member1, IRCChannelUser *member2) { NSString *nickname1 = member1.user.nickname; NSString *nickname2 = member2.user.nickname; return [nickname1 caseInsensitiveCompare:nickname2]; }]; } /* Join user objects into string */ NSMutableString *resultString = [NSMutableString string]; for (IRCChannelUser *member in memberList) { if (displayRank) { [resultString appendString:member.mark]; } [resultString appendString:member.user.nickname]; [resultString appendString:@" "]; } [client printDebugInformation:[resultString copy] inChannel:channel]; } - (void)findAllUsersMatchingString:(NSString *)matchString inChannel:(IRCChannel *)channel onClient:(IRCClient *)client { NSParameterAssert(matchString != nil); NSParameterAssert(channel != nil); NSParameterAssert(client != nil); BOOL hasSearchCondition = (matchString.length > 0); NSArray *memberList = channel.memberList; if (memberList.count == 0) { if (hasSearchCondition) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[n1j-tp]", channel.name, matchString) inChannel:channel]; } else { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[1ab-27]", channel.name) inChannel:channel]; } return; } NSMutableArray *membersMatched = [NSMutableArray array]; for (IRCChannelUser *member in memberList) { NSString *hostmask = member.user.hostmask; if (hostmask == nil) { continue; } if (hasSearchCondition) { if ([hostmask containsIgnoringCase:matchString] == NO) { continue; } } [membersMatched addObject:member]; } if (membersMatched.count <= 0) { if (hasSearchCondition) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[n1j-tp]", channel.name, matchString) inChannel:channel]; } else { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[1ab-27]", channel.name) inChannel:channel]; } return; } if (hasSearchCondition) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[oq7-mg]", membersMatched.count, channel.name, matchString) inChannel:channel]; } else { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[nn7-6s]", membersMatched.count, channel.name) inChannel:channel]; } [membersMatched sortUsingComparator:^NSComparisonResult(IRCChannelUser *member1, IRCChannelUser *member2) { NSString *nickname1 = member1.user.nickname; NSString *nickname2 = member2.user.nickname; return [nickname1 caseInsensitiveCompare:nickname2]; }]; for (IRCChannelUser *member in membersMatched) { NSString *resultString = [NSString stringWithFormat:@"%@ -> %@", member.user.nickname, member.user.hostmask]; [client printDebugInformation:resultString inChannel:channel]; } } - (void)findAllClonesInChannel:(IRCChannel *)channel onClient:(IRCClient *)client { NSParameterAssert(channel != nil); NSParameterAssert(client != nil); NSMutableDictionary *members = [NSMutableDictionary dictionary]; /* Populate our list by matching an array of users to that of the address. */ for (IRCChannelUser *member in channel.memberList) { NSString *address = member.user.address; if (address == nil) { continue; } NSString *nickname = member.user.nickname; NSArray *clones = members[address]; if (clones) { clones = [clones arrayByAddingObject:nickname]; members[address] = clones; } else { members[address] = @[nickname]; } } /* Filter the new list by removing users with less than two matches. */ NSArray *memberHosts = members.allKeys; for (NSString *memberHost in memberHosts) { NSArray *clones = [members arrayForKey:memberHost]; if (clones.count < 2) { [members removeObjectForKey:memberHost]; } } /* No clones found */ if (members.count == 0) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[gxq-47]") inChannel:channel]; return; } /* Build result string */ [client printDebugInformation:TPILocalizedString(@"BasicLanguage[iaa-5v]", members.count, channel.name) inChannel:channel]; for (NSString *memberHost in members) { NSArray *clones = [members arrayForKey:memberHost]; NSString *clonesString = [clones componentsJoinedByString:@", "]; NSString *resultString = [NSString stringWithFormat:@"*!*@%@ -> %@", memberHost, clonesString]; [client printDebugInformation:resultString inChannel:channel]; } } - (void)appendPluralOrSingular:(NSMutableString *)resultString valueToken:(NSString *)valueToken value:(NSInteger)value { NSParameterAssert(resultString != nil); NSParameterAssert(valueToken != nil); NSString *valueKey = nil; if (value == 1) { valueKey = [NSString stringWithFormat:@"BasicLanguage[%@-1]", valueToken]; } else { valueKey = [NSString stringWithFormat:@"BasicLanguage[%@-2]", valueToken]; } [resultString appendString:TPILocalizedString(valueKey, value)]; } - (void)bragInChannel:(IRCChannel *)channel onClient:(IRCClient *)client { NSParameterAssert(channel != nil); NSParameterAssert(client != nil); NSUInteger operCount = 0; NSUInteger channelOpCount = 0; NSUInteger channelHalfopCount = 0; NSUInteger channelVoiceCount = 0; NSUInteger channelCount = 0; NSUInteger networkCount = 0; NSUInteger powerOverCount = 0; for (IRCClient *cl in worldController().clientList) { if (cl.isConnected == NO) { continue; } networkCount++; IRCUser *localUser = cl.myself; if (cl.userIsIRCop || localUser.isIRCop) { operCount++; } NSMutableArray *trackedUsers = [NSMutableArray new]; for (IRCChannel *ch in cl.channelList) { if (ch.isActive == NO || ch.isChannel == NO) { continue; } channelCount += 1; IRCChannelUser *myself = [ch findMember:cl.userNickname]; IRCUserRank myRanks = myself.ranks; BOOL IHaveModeQ = ((myRanks & IRCUserRankChannelOwner) == IRCUserRankChannelOwner); BOOL IHaveModeA = ((myRanks & IRCUserRankSuperOperator) == IRCUserRankSuperOperator); BOOL IHaveModeO = ((myRanks & IRCUserRankNormalOperator) == IRCUserRankNormalOperator); BOOL IHaveModeH = ((myRanks & IRCUserRankHalfOperator) == IRCUserRankHalfOperator); BOOL IHaveModeV = ((myRanks & IRCUserRankVoiced) == IRCUserRankVoiced); if (IHaveModeQ || IHaveModeA || IHaveModeO) { channelOpCount++; } else if (IHaveModeH) { channelHalfopCount++; } else if (IHaveModeV) { channelVoiceCount++; } for (IRCChannelUser *member in ch.memberList) { if ([member isEqual:myself]) { continue; } BOOL addUser = NO; IRCUserRank userRanks = member.ranks; BOOL UserHasModeQ = ((userRanks & IRCUserRankChannelOwner) == IRCUserRankChannelOwner); BOOL UserHasModeA = ((userRanks & IRCUserRankSuperOperator) == IRCUserRankSuperOperator); BOOL UserHasModeO = ((userRanks & IRCUserRankNormalOperator) == IRCUserRankNormalOperator); BOOL UserHasModeH = ((userRanks & IRCUserRankHalfOperator) == IRCUserRankHalfOperator); if (cl.userIsIRCop && member.user.isIRCop == NO) { addUser = YES; } else if (IHaveModeQ && UserHasModeQ == NO) { addUser = YES; } else if (IHaveModeA && UserHasModeQ == NO && UserHasModeA == NO) { addUser = YES; } else if (IHaveModeO && UserHasModeQ == NO && UserHasModeA == NO && UserHasModeO == NO) { addUser = YES; } else if (IHaveModeH && UserHasModeQ == NO && UserHasModeA == NO && UserHasModeO == NO && UserHasModeH == NO) { addUser = YES; } if (addUser) { NSString *nickname = member.user.nickname; if ([trackedUsers containsObject:nickname] == NO) { [trackedUsers addObject:nickname]; powerOverCount++; } } } } } NSMutableString *resultString = [NSMutableString string]; [self appendPluralOrSingular:resultString valueToken:@"30l-sx" value:channelCount]; [self appendPluralOrSingular:resultString valueToken:@"rks-0t" value:networkCount]; if (powerOverCount == 0) { [resultString appendString:TPILocalizedString(@"BasicLanguage[jpi-po]")]; } else { [self appendPluralOrSingular:resultString valueToken:@"614-ac" value:operCount]; [self appendPluralOrSingular:resultString valueToken:@"qne-b5" value:channelOpCount]; [self appendPluralOrSingular:resultString valueToken:@"431-yv" value:channelHalfopCount]; [self appendPluralOrSingular:resultString valueToken:@"x1m-jp" value:channelVoiceCount]; [self appendPluralOrSingular:resultString valueToken:@"ny4-wd" value:powerOverCount]; } [client sendPrivmsg:[resultString copy] toChannel:channel]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/User Insights/Resources/Language Files/en.lproj/BasicLanguage.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2013 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "5dx-rs" = "No users found in %@"; "gxq-47" = "No clones found"; "iaa-5v" = "%1$lu clone(s) found in %2$@"; "r6x-ih" = "Bad input"; "nn7-6s" = "%1$lu user(s) found in %2$@ with a cached hostmask."; "oq7-mg" = "%1$lu user(s) found in %2$@ with a cached hostmask containing the search string “%3$@”"; "1ab-27" = "No users found in %@ with a cached hostmask."; "n1j-tp" = "No users found in %1$@ with a cached hostmask containing the search string “%2$@”"; "30l-sx-1" = "I am in %ld channel while connected to "; "30l-sx-2" = "I am in %ld channels while connected to "; "rks-0t-1" = "%ld network. "; "rks-0t-2" = "%ld networks. "; "614-ac-1" = "I have %ld o:line, "; "614-ac-2" = "I have %ld o:lines, "; "qne-b5-1" = "%ld op, "; "qne-b5-2" = "%ld ops, "; "431-yv-1" = "%ld halfop, "; "431-yv-2" = "%ld halfops, "; "x1m-jp-1" = "and %ld voice "; "x1m-jp-2" = "and %ld voices "; "ny4-wd-1" = "with power over %ld user."; "ny4-wd-2" = "with power over %ld unique users."; "jpi-po" = "I have no power whatsoever."; ================================================ FILE: Sources/Plugins/User Insights/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.1 NSHumanReadableCopyright Copyright © 2013 - 2018 Codeux Software, LLC. All rights reserved. MinimumTextualVersion 7.2.4 NSPrincipalClass TPIUserInsights ================================================ FILE: Sources/Plugins/User Insights/User Insights Extension.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C06DE7C20EC45F10055D09A /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C06DE7B20EC45F10055D09A /* CoreFoundation.framework */; }; 4C36AADC20F27CFA007CA939 /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AADA20F27CFA007CA939 /* BasicLanguage.strings */; }; 4C46A09B20EC6B0900094EA4 /* TPIUserInsights.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A09920EC6B0900094EA4 /* TPIUserInsights.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C06DE7B20EC45F10055D09A /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; 4C36AADB20F27CFA007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BasicLanguage.strings; sourceTree = ""; }; 4C46A09920EC6B0900094EA4 /* TPIUserInsights.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPIUserInsights.m; sourceTree = ""; }; 4C46A09A20EC6B0900094EA4 /* TPIUserInsights.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPIUserInsights.h; sourceTree = ""; }; 4C46A09F20EC6B1800094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4CE1433920EBF4F3000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1433A20EBF4F3000E01C9 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4CE1433E20EBF4F3000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE1434C20EBF4F3000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1434D20EBF4F3000E01C9 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4CE1434E20EBF4F3000E01C9 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4CE1435020EBF4F3000E01C9 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4CE1435320EBF4F3000E01C9 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4CE1436120EBF4F3000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1436220EBF4F3000E01C9 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4CE1436620EBF4F3000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 8D576316048677EA00EA77CD /* User Insights.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "User Insights.bundle"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4C06DE7C20EC45F10055D09A /* CoreFoundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* BragSpam */ = { isa = PBXGroup; children = ( 4C46A09820EC6B0900094EA4 /* Classes */, 089C167CFE841241C02AAC07 /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = BragSpam; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( 4C06DE7B20EC45F10055D09A /* CoreFoundation.framework */, ); name = Frameworks; sourceTree = ""; }; 089C167CFE841241C02AAC07 /* Resources */ = { isa = PBXGroup; children = ( 4C6918A016E3277A00BDE3A8 /* Configurations */, 4C46A09C20EC6B1800094EA4 /* Language Files */, 4C46A09E20EC6B1800094EA4 /* Property Lists */, ); path = Resources; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* User Insights.bundle */, ); name = Products; sourceTree = ""; }; 4C46A09820EC6B0900094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A09A20EC6B0900094EA4 /* TPIUserInsights.h */, 4C46A09920EC6B0900094EA4 /* TPIUserInsights.m */, ); path = Classes; sourceTree = ""; }; 4C46A09C20EC6B1800094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C36AADA20F27CFA007CA939 /* BasicLanguage.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C46A09E20EC6B1800094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A09F20EC6B1800094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C6918A016E3277A00BDE3A8 /* Configurations */ = { isa = PBXGroup; children = ( 4CE1433520EBF4F3000E01C9 /* Build */, ); name = Configurations; path = ../../../Configurations; sourceTree = SOURCE_ROOT; }; 4CE1433520EBF4F3000E01C9 /* Build */ = { isa = PBXGroup; children = ( 4CE1434920EBF4F3000E01C9 /* Common */, 4CE1435E20EBF4F3000E01C9 /* Debug */, 4CE1433620EBF4F3000E01C9 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4CE1433620EBF4F3000E01C9 /* Standard Release */ = { isa = PBXGroup; children = ( 4CE1433E20EBF4F3000E01C9 /* Enabled Features.xcconfig */, 4CE1433A20EBF4F3000E01C9 /* Textual Extensions.xcconfig */, 4CE1433920EBF4F3000E01C9 /* Textual.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4CE1434920EBF4F3000E01C9 /* Common */ = { isa = PBXGroup; children = ( 4CE1435320EBF4F3000E01C9 /* Foundation Debug.xcconfig */, 4CE1435020EBF4F3000E01C9 /* Foundation.xcconfig */, 4CE1434D20EBF4F3000E01C9 /* Preserve Symbols.xcconfig */, 4CE1434E20EBF4F3000E01C9 /* Textual Extensions.xcconfig */, 4CE1434C20EBF4F3000E01C9 /* Textual.xcconfig */, ); path = Common; sourceTree = ""; }; 4CE1435E20EBF4F3000E01C9 /* Debug */ = { isa = PBXGroup; children = ( 4CE1436620EBF4F3000E01C9 /* Enabled Features.xcconfig */, 4CE1436220EBF4F3000E01C9 /* Textual Extensions.xcconfig */, 4CE1436120EBF4F3000E01C9 /* Textual.xcconfig */, ); path = Debug; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* User Insights Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "User Insights Extension" */; buildPhases = ( 8D57630F048677EA00EA77CD /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "User Insights Extension"; productInstallPath = "$(HOME)/Library/Bundles"; productName = BragSpam; productReference = 8D576316048677EA00EA77CD /* User Insights.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; TargetAttributes = { 8D57630D048677EA00EA77CD = { DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "User Insights Extension" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( en, Base, ); mainGroup = 089C166AFE841209C02AAC07 /* BragSpam */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* User Insights Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 8D57630F048677EA00EA77CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C36AADC20F27CFA007CA939 /* BasicLanguage.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A09B20EC6B0900094EA4 /* TPIUserInsights.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C36AADA20F27CFA007CA939 /* BasicLanguage.strings */ = { isa = PBXVariantGroup; children = ( 4C36AADB20F27CFA007CA939 /* en */, ); name = BasicLanguage.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1DEB911B08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-UserInsights"; PRODUCT_NAME = "User Insights"; }; name = Debug; }; 1DEB911C08733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-UserInsights"; PRODUCT_NAME = "User Insights"; }; name = Release; }; 1DEB911F08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE1436220EBF4F3000E01C9 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Debug; }; 1DEB912008733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE1433A20EBF4F3000E01C9 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4C9580181FA41EE300F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (App Store)"; }; 4C9580191FA41EE300F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-UserInsights"; PRODUCT_NAME = "User Insights"; }; name = "Release (App Store)"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "User Insights Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911B08733D790010E9CD /* Debug */, 1DEB911C08733D790010E9CD /* Release */, 4C9580191FA41EE300F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "User Insights Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911F08733D790010E9CD /* Debug */, 1DEB912008733D790010E9CD /* Release */, 4C9580181FA41EE300F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Plugins/Wiki Link Parser/Classes/TPIWikiStyleLinkParser.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" @interface TPIWikiStyleLinkParser : NSObject @end ================================================ FILE: Sources/Plugins/Wiki Link Parser/Classes/TPIWikiStyleLinkParser.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPIWikiStyleLinkParser.h" /* Link prefix storage is a remnant of older times. There are better ways to store them other than an array with specific index values, but because this plugin is optional and rarely needs any maintaining, its not a priority to make it any better. */ #define _indexChannelId 0 #define _indexLinkPrefix 1 #define _linkMatchRegex @"\\[\\[([^\\]]+)\\]\\]" @interface TPIWikiStyleLinkParser () @property (nonatomic, copy) NSArray *linkPrefixes; @property (nonatomic, strong) IBOutlet NSView *preferencePane; @property (nonatomic, strong) IBOutlet NSWindow *rnewConditionWindow; @property (nonatomic, weak) IBOutlet TVCValidatedTextField *rnewConditionLinkPrefixField; @property (nonatomic, weak) IBOutlet NSPopUpButton *rnewConditionChannelPopup; @property (nonatomic, weak) IBOutlet NSButton *rnewConditionSaveButton; @property (nonatomic, weak) IBOutlet NSButton *rnewConditionCancelButton; @property (nonatomic, weak) IBOutlet NSButton *addConditionButton; @property (nonatomic, weak) IBOutlet NSButton *removeConditionButton; @property (nonatomic, weak) IBOutlet NSTableView *linkPrefixesTable; - (void)addCondition:(id)sender; - (void)removeCondition:(id)sender; - (void)saveNewCondition:(id)sender; - (void)cancelNewCondition:(id)sender; - (void)updateNewConditionWindowSaveButton:(id)sender; @end @implementation TPIWikiStyleLinkParser #pragma mark - #pragma mark Init - (void)pluginLoadedIntoMemory { [self performBlockOnMainThread:^{ [self linkPrefixesStorageRead]; [TPIBundleFromClass() loadNibNamed:@"TPIWikiStyleLinkParser" owner:self topLevelObjects:nil]; [self.rnewConditionLinkPrefixField setStringValueIsInvalidOnEmpty:YES]; [self.rnewConditionLinkPrefixField setTextDidChangeCallback:self]; [self.rnewConditionLinkPrefixField setValidationBlock:^NSString *(NSString *currentValue) { NSURL *URLValue = [NSURL URLWithString:currentValue]; if (URLValue == nil) { return TPILocalizedString(@"BasicLanguage[535-5j]"); } return nil; }]; [self updateRemoveConditionButton]; }]; } #pragma mark - #pragma mark Server Input. - (NSString *)willRenderMessage:(NSString *)newMessage forViewController:(TVCLogController *)logController lineType:(TVCLogLineType)lineType memberType:(TVCLogLineMemberType)memberType { if ([self processWikiStyleLinks] == NO) { return nil; } /* Only work on plain text messages */ if (lineType == TVCLogLineTypePrivateMessage || lineType == TVCLogLineTypeAction) { IRCChannel *channel = [logController associatedChannel]; NSString *linkPrefix = [self linkPrefixFromId:[channel uniqueIdentifier]]; if (linkPrefix == nil) { return nil; } NSMutableString *muteString = [newMessage mutableCopy]; while (1 == 1) { NSRange linkRange = [XRRegularExpression string:muteString rangeOfRegex:_linkMatchRegex]; if (linkRange.location == NSNotFound) { break; } else if (linkRange.length < 5) { continue; } /* Get inside of brackets */ NSRange cutRange = NSMakeRange((linkRange.location + 2), (linkRange.length - 4)); NSString *linkInside = [muteString substringWithRange:cutRange]; /* Get the left side */ NSInteger insideBarPosition = [linkInside stringPosition:@"|"]; if (insideBarPosition > 0) { linkInside = [linkInside substringToIndex:insideBarPosition]; linkInside = [linkInside trim]; } linkInside = [linkInside percentEncodedString]; linkInside = [linkPrefix stringByAppendingString:linkInside]; [muteString replaceCharactersInRange:linkRange withString:linkInside]; } return muteString; } return nil; } #pragma mark - #pragma mark Preference Pane. - (NSString *)pluginPreferencesPaneMenuItemName { return TPILocalizedString(@"BasicLanguage[0vq-yu]"); } - (NSView *)pluginPreferencesPaneView { return self.preferencePane; } #pragma mark - #pragma mark Table View delegate - (void)tableViewSelectionDidChange:(NSNotification *)notification { [self updateRemoveConditionButton]; } - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return [self.linkPrefixes count]; } - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { NSString *columnId = [tableColumn identifier]; NSArray *entryInfo = self.linkPrefixes[row]; if ([columnId isEqualToString:@"channel"]) { NSString *entryName = [self channelNameFromId:entryInfo[_indexChannelId]]; if (entryName == nil) { return TPILocalizedString(@"BasicLanguage[sb5-c9]"); } return entryName; } else if ([columnId isEqualToString:@"link"]) { return entryInfo[_indexLinkPrefix]; } return nil; } #pragma mark - #pragma mark Table View Actions - (void)removeCondition:(id)sender { NSInteger selectedRow = [self.linkPrefixesTable selectedRow]; if (selectedRow < 0) { return; } NSMutableArray *linkPrefixes = [self.linkPrefixes mutableCopy]; [linkPrefixes removeObjectAtIndex:selectedRow]; self.linkPrefixes = linkPrefixes; [self linkPrefixesStorageSave]; [self.linkPrefixesTable reloadData]; } - (void)updateRemoveConditionButton { NSInteger selectedRow = [self.linkPrefixesTable selectedRow]; [self.removeConditionButton setEnabled:(selectedRow >= 0)]; } #pragma mark - #pragma mark Condition Management - (void)validatedTextFieldTextDidChange:(id)sender { if (sender == self.rnewConditionLinkPrefixField) { [self updateNewConditionWindowSaveButton:nil]; } } - (void)addCondition:(id)sender { [self.rnewConditionLinkPrefixField setStringValue:NSStringEmptyPlaceholder]; [self.rnewConditionChannelPopup removeAllItems]; [self.addConditionButton setEnabled:NO]; [self.removeConditionButton setEnabled:NO]; NSMutableArray *listOfTrackedIds = [NSMutableArray array]; for (NSArray *entry in self.linkPrefixes) { [listOfTrackedIds addObject:entry[_indexChannelId]]; } for (IRCClient *u in [worldController() clientList]) { /* Record information about the channel list */ NSArray *channelList = [u channelList]; NSInteger channelCount = [channelList count]; if (channelCount == 0) { continue; } /* Create new menu item for client */ NSMenuItem *clientMenuItem = [NSMenuItem new]; [clientMenuItem setEnabled:NO]; [clientMenuItem setTitle:[u name]]; [[self.rnewConditionChannelPopup menu] addItem:clientMenuItem]; /* Filter channel list */ for (IRCChannel *c in channelList) { if ([c isChannel] == NO) { continue; } NSString *channelId = [c uniqueIdentifier]; /* Do we already track it? */ BOOL channelTracked = [listOfTrackedIds containsObject:channelId]; if (channelTracked) { channelCount -= 1; // Decrease channel count by one continue; } /* Create new menu item for channel */ NSString *channelName = [NSString stringWithFormat:@" %@", [c name]]; NSMenuItem *channelMenuItem = [NSMenuItem new]; [channelMenuItem setEnabled:YES]; [channelMenuItem setTitle:channelName]; [channelMenuItem setUserInfo:channelId]; [[self.rnewConditionChannelPopup menu] addItem:channelMenuItem]; } /* Remove the client title? */ if (channelCount <= 0) { [[self.rnewConditionChannelPopup menu] removeItem:clientMenuItem]; } } listOfTrackedIds = nil; /* Present window */ [NSApp beginSheet:self.rnewConditionWindow modalForWindow:[NSApp keyWindow] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:nil]; [self.rnewConditionWindow makeFirstResponder:self.rnewConditionLinkPrefixField]; } - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { [sheet close]; } - (void)saveNewCondition:(id)sender { if ([self validateLinkPrefix] == NO) { return; } NSMutableArray *linkPrefixes = [self.linkPrefixes mutableCopy]; NSString *linkPrefix = [self.rnewConditionLinkPrefixField stringValue]; NSMenuItem *channelMenuItem = [self.rnewConditionChannelPopup selectedItem]; NSString *channelId = [channelMenuItem userInfo]; [linkPrefixes addObjectWithoutDuplication:@[channelId, linkPrefix]]; self.linkPrefixes = linkPrefixes; [self linkPrefixesStorageSave]; [self.linkPrefixesTable reloadData]; [self cancelNewCondition:nil]; } - (void)cancelNewCondition:(id)sender { [self.addConditionButton setEnabled:YES]; [self.removeConditionButton setEnabled:YES]; [NSApp endSheet:self.rnewConditionWindow]; [self updateRemoveConditionButton]; } - (void)updateNewConditionWindowSaveButton:(id)sender { BOOL condition2 = [[self.rnewConditionChannelPopup selectedItem] isEnabled]; [self.rnewConditionSaveButton setEnabled:condition2]; } - (BOOL)validateLinkPrefix { BOOL condition1 = [self.rnewConditionLinkPrefixField valueIsValid]; if (condition1 == NO) { [self.rnewConditionLinkPrefixField showValidationErrorPopover]; return NO; } return YES; } #pragma mark - #pragma mark Utilities - (BOOL)processWikiStyleLinks { return [RZUserDefaults() boolForKey:@"Wiki-style Link Parser Extension -> Service Enabled"]; } - (void)linkPrefixesStorageRead { NSArray *linkPrefixes = [RZUserDefaults() arrayForKey:@"Wiki-style Link Parser Extension -> Link Prefixes"]; if (linkPrefixes == nil) { linkPrefixes = @[]; } self.linkPrefixes = linkPrefixes; } - (void)linkPrefixesStorageSave { [RZUserDefaults() setObject:self.linkPrefixes forKey:@"Wiki-style Link Parser Extension -> Link Prefixes"]; } - (NSString *)channelNameFromId:(NSString *)itemId { for (IRCClient *u in [worldController() clientList]) { for (IRCChannel *c in [u channelList]) { if ([[c uniqueIdentifier] isEqualToString:itemId]) { return [c name]; } } } return nil; } - (NSString *)linkPrefixFromId:(NSString *)itemId { for (NSArray *entry in self.linkPrefixes) { if ([entry[_indexChannelId] isEqualToString:itemId]) { return entry[_indexLinkPrefix]; } } return nil; } @end ================================================ FILE: Sources/Plugins/Wiki Link Parser/Resources/Language Files/en.lproj/BasicLanguage.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2015 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Next unused key: 1004 */ "0vq-yu" = "Wiki-style Link Parser"; "sb5-c9" = "Channel Does Not Exist"; "535-5j" = "Please enter a properly formatted URL."; ================================================ FILE: Sources/Plugins/Wiki Link Parser/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.9 NSHumanReadableCopyright Copyright © 2015 - 2020 Codeux Software, LLC. All rights reserved. MinimumTextualVersion 7.2.4 NSPrincipalClass TPIWikiStyleLinkParser ================================================ FILE: Sources/Plugins/Wiki Link Parser/Resources/User Interface/en.lproj/TPIWikiStyleLinkParser.xib ================================================ This addon replaces Wiki-markup title blocks, similar to [[<title>]], with a link to the article they represent. Each channel is assigned a “Link Prefix” which is the URL prepended to the title of the article that will be linked to. Replaced title blocks are only visible to you. ================================================ FILE: Sources/Plugins/Wiki Link Parser/Wiki-style Link Parser Extension.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 4C36AACB20F27B2E007CA939 /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAC920F27B2E007CA939 /* BasicLanguage.strings */; }; 4C36AACE20F27B85007CA939 /* TPIWikiStyleLinkParser.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AACC20F27B85007CA939 /* TPIWikiStyleLinkParser.xib */; }; 4C46A14720EC6E4E00094EA4 /* TPIWikiStyleLinkParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A14520EC6E4D00094EA4 /* TPIWikiStyleLinkParser.m */; }; 4CBE7B481A9146E2008FB230 /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CBE7B471A9146E2008FB230 /* CocoaExtensions.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C36AACA20F27B2E007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BasicLanguage.strings; sourceTree = ""; }; 4C36AACD20F27B85007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/TPIWikiStyleLinkParser.xib; sourceTree = ""; }; 4C46A0E420EC6DD900094EA4 /* Inherited.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Inherited.entitlements; sourceTree = ""; }; 4C46A0E820EC6DD900094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A0E920EC6DD900094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A0ED20EC6DD900094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A0F020EC6DD900094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A0F120EC6DD900094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A0F520EC6DD900094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A0F920EC6DD900094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A0FA20EC6DD900094EA4 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4C46A0FB20EC6DD900094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A0FD20EC6DD900094EA4 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4C46A10020EC6DD900094EA4 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4C46A10420EC6DD900094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A10520EC6DD900094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A10920EC6DD900094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A10C20EC6DD900094EA4 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C46A10D20EC6DD900094EA4 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C46A11120EC6DD900094EA4 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C46A13B20EC6E3C00094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C46A14520EC6E4D00094EA4 /* TPIWikiStyleLinkParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPIWikiStyleLinkParser.m; sourceTree = ""; }; 4C46A14620EC6E4D00094EA4 /* TPIWikiStyleLinkParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPIWikiStyleLinkParser.h; sourceTree = ""; }; 4CBE7B471A9146E2008FB230 /* CocoaExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaExtensions.framework; path = "../../../.tmp/SharedBuildProducts-Frameworks/CocoaExtensions.framework"; sourceTree = ""; }; 8D576316048677EA00EA77CD /* WikiStyleLinkParser.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WikiStyleLinkParser.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4CBE7B481A9146E2008FB230 /* CocoaExtensions.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* BragSpam */ = { isa = PBXGroup; children = ( 4C46A14420EC6E4D00094EA4 /* Classes */, 089C167CFE841241C02AAC07 /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = BragSpam; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( 4CBE7B471A9146E2008FB230 /* CocoaExtensions.framework */, ); name = Frameworks; sourceTree = ""; }; 089C167CFE841241C02AAC07 /* Resources */ = { isa = PBXGroup; children = ( 4C46A0E120EC6DD900094EA4 /* Configurations */, 4C46A13C20EC6E3C00094EA4 /* Language Files */, 4C46A13A20EC6E3C00094EA4 /* Property Lists */, 4C46A13E20EC6E3C00094EA4 /* User Interface */, ); path = Resources; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* WikiStyleLinkParser.bundle */, ); name = Products; sourceTree = ""; }; 4C46A0E120EC6DD900094EA4 /* Configurations */ = { isa = PBXGroup; children = ( 4C46A0E520EC6DD900094EA4 /* Build */, 4C46A0E320EC6DD900094EA4 /* Sandbox */, ); name = Configurations; path = ../../../Configurations; sourceTree = SOURCE_ROOT; }; 4C46A0E320EC6DD900094EA4 /* Sandbox */ = { isa = PBXGroup; children = ( 4C46A0E420EC6DD900094EA4 /* Inherited.entitlements */, ); path = Sandbox; sourceTree = ""; }; 4C46A0E520EC6DD900094EA4 /* Build */ = { isa = PBXGroup; children = ( 4C46A0EE20EC6DD900094EA4 /* App Store Release */, 4C46A0F720EC6DD900094EA4 /* Common */, 4C46A10A20EC6DD900094EA4 /* Debug */, 4C46A0E620EC6DD900094EA4 /* Standard Release */, 4C46A10220EC6DD900094EA4 /* Standard Release Sandboxed */, ); path = Build; sourceTree = ""; }; 4C46A0E620EC6DD900094EA4 /* Standard Release */ = { isa = PBXGroup; children = ( 4C46A0ED20EC6DD900094EA4 /* Enabled Features.xcconfig */, 4C46A0E920EC6DD900094EA4 /* Textual Extensions.xcconfig */, 4C46A0E820EC6DD900094EA4 /* Textual.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4C46A0EE20EC6DD900094EA4 /* App Store Release */ = { isa = PBXGroup; children = ( 4C46A0F520EC6DD900094EA4 /* Enabled Features.xcconfig */, 4C46A0F120EC6DD900094EA4 /* Textual Extensions.xcconfig */, 4C46A0F020EC6DD900094EA4 /* Textual.xcconfig */, ); path = "App Store Release"; sourceTree = ""; }; 4C46A0F720EC6DD900094EA4 /* Common */ = { isa = PBXGroup; children = ( 4C46A10020EC6DD900094EA4 /* Foundation Debug.xcconfig */, 4C46A0FD20EC6DD900094EA4 /* Foundation.xcconfig */, 4C46A0FA20EC6DD900094EA4 /* Preserve Symbols.xcconfig */, 4C46A0FB20EC6DD900094EA4 /* Textual Extensions.xcconfig */, 4C46A0F920EC6DD900094EA4 /* Textual.xcconfig */, ); path = Common; sourceTree = ""; }; 4C46A10220EC6DD900094EA4 /* Standard Release Sandboxed */ = { isa = PBXGroup; children = ( 4C46A10920EC6DD900094EA4 /* Enabled Features.xcconfig */, 4C46A10520EC6DD900094EA4 /* Textual Extensions.xcconfig */, 4C46A10420EC6DD900094EA4 /* Textual.xcconfig */, ); path = "Standard Release Sandboxed"; sourceTree = ""; }; 4C46A10A20EC6DD900094EA4 /* Debug */ = { isa = PBXGroup; children = ( 4C46A11120EC6DD900094EA4 /* Enabled Features.xcconfig */, 4C46A10D20EC6DD900094EA4 /* Textual Extensions.xcconfig */, 4C46A10C20EC6DD900094EA4 /* Textual.xcconfig */, ); path = Debug; sourceTree = ""; }; 4C46A13A20EC6E3C00094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A13B20EC6E3C00094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A13C20EC6E3C00094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C36AAC920F27B2E007CA939 /* BasicLanguage.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C46A13E20EC6E3C00094EA4 /* User Interface */ = { isa = PBXGroup; children = ( 4C36AACC20F27B85007CA939 /* TPIWikiStyleLinkParser.xib */, ); path = "User Interface"; sourceTree = ""; }; 4C46A14420EC6E4D00094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A14620EC6E4D00094EA4 /* TPIWikiStyleLinkParser.h */, 4C46A14520EC6E4D00094EA4 /* TPIWikiStyleLinkParser.m */, ); path = Classes; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* Wiki-style Link Parser Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Wiki-style Link Parser Extension" */; buildPhases = ( 8D57630F048677EA00EA77CD /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "Wiki-style Link Parser Extension"; productInstallPath = "$(HOME)/Library/Bundles"; productName = BragSpam; productReference = 8D576316048677EA00EA77CD /* WikiStyleLinkParser.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1100; TargetAttributes = { 8D57630D048677EA00EA77CD = { DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Wiki-style Link Parser Extension" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( en, Base, ); mainGroup = 089C166AFE841209C02AAC07 /* BragSpam */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* Wiki-style Link Parser Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 8D57630F048677EA00EA77CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C36AACE20F27B85007CA939 /* TPIWikiStyleLinkParser.xib in Resources */, 4C36AACB20F27B2E007CA939 /* BasicLanguage.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A14720EC6E4E00094EA4 /* TPIWikiStyleLinkParser.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C36AAC920F27B2E007CA939 /* BasicLanguage.strings */ = { isa = PBXVariantGroup; children = ( 4C36AACA20F27B2E007CA939 /* en */, ); name = BasicLanguage.strings; sourceTree = ""; }; 4C36AACC20F27B85007CA939 /* TPIWikiStyleLinkParser.xib */ = { isa = PBXVariantGroup; children = ( 4C36AACD20F27B85007CA939 /* en */, ); name = TPIWikiStyleLinkParser.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1DEB911B08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-WikiStyleLinkParser"; PRODUCT_NAME = WikiStyleLinkParser; }; name = Debug; }; 1DEB911C08733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-WikiStyleLinkParser"; PRODUCT_NAME = WikiStyleLinkParser; }; name = Release; }; 1DEB911F08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C46A10D20EC6DD900094EA4 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Debug; }; 1DEB912008733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C46A0E920EC6DD900094EA4 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4C9580241FA41FA800F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C46A0F120EC6DD900094EA4 /* Textual Extensions.xcconfig */; buildSettings = { }; name = "Release (App Store)"; }; 4C9580251FA41FA800F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-WikiStyleLinkParser"; PRODUCT_NAME = WikiStyleLinkParser; }; name = "Release (App Store)"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "Wiki-style Link Parser Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911B08733D790010E9CD /* Debug */, 1DEB911C08733D790010E9CD /* Release */, 4C9580251FA41FA800F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "Wiki-style Link Parser Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911F08733D790010E9CD /* Debug */, 1DEB912008733D790010E9CD /* Release */, 4C9580241FA41FA800F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Plugins/Wiki Link Parser/Wiki-style Link Parser Extension.xcodeproj/xcshareddata/xcschemes/Wiki-style Link Parser Extension.xcscheme ================================================ ================================================ FILE: Sources/Plugins/ZNC Additions/Classes/TPI_ZNCAdditions.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2011 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "Textual.h" NS_ASSUME_NONNULL_BEGIN @interface TPI_ZNCAdditions : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/ZNC Additions/Classes/TPI_ZNCAdditions.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2011 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "TPI_ZNCAdditions.h" #import "IRCClientPrivate.h" #import "TVCMainWindowPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TPI_ZNCAdditions #pragma mark - #pragma mark Plugin API - (void)didReceiveServerInput:(THOPluginDidReceiveServerInputConcreteObject *)inputObject onClient:(IRCClient *)client { if (client.isConnectedToZNC == NO) { return; } NSString *sender = inputObject.senderNickname; NSString *message = inputObject.messageSequence; if ([client nickname:sender isZNCUser:@"status"] && [message hasPrefix:@"Disconnected from IRC"]) { /* We listen for ZNC disconnects so that we can terminate channels when we disconnect from the server ZNC was connected to. ZNC does not localize itself so detecting these disconnects is not very hard... */ XRPerformBlockSynchronouslyOnMainQueue(^{ [self handleIRCSideDisconnect:client]; }); } } - (void)userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString { XRPerformBlockAsynchronouslyOnMainQueue(^{ [self _userInputCommandInvokedOnClient:client commandString:commandString messageString:messageString]; }); } - (void)_userInputCommandInvokedOnClient:(IRCClient *)client commandString:(NSString *)commandString messageString:(NSString *)messageString { /* Throw error if user tries to invoke a command that requires the user to be connected to a ZNC bouncer */ if (client.isConnectedToZNC == NO) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[xex-nl]")]; return; } /* Process commands */ if ([commandString isEqualToStringIgnoringCase:@"ZNCCERT"]) { /* Textual is designed not to import partial content. It will either return the complete certificate chain at this point, or nil. */ NSData *certificateData = client.zncBouncerCertificateChainData; if (certificateData == nil) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[moh-hg]")]; return; } /* Given the raw, base64 encoded data, convert it into certificate objects. */ SecItemImportExportKeyParameters importParameters; importParameters.flags = kSecKeyNoAccessControl; importParameters.version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION; importParameters.accessRef = NULL; importParameters.alertTitle = NULL; importParameters.alertPrompt = NULL; importParameters.passphrase = NULL; importParameters.keyAttributes = NULL; importParameters.keyUsage = NULL; SecExternalItemType itemType = kSecItemTypeCertificate; SecExternalFormat externalFormat = kSecFormatPEMSequence; int flags = 0; CFArrayRef certificateArray = NULL; OSStatus operationStatus = SecItemImport((__bridge CFDataRef)(certificateData), NULL, &externalFormat, &itemType, flags, &importParameters, NULL, &certificateArray); /* Display an error or hand the certificate chain off to Apple's own APIs to display them in a dialog. */ if (operationStatus != noErr) { [client printDebugInformation:TPILocalizedString(@"BasicLanguage[wco-zv]")]; return; } SFCertificateTrustPanel *panel = [SFCertificateTrustPanel new]; [panel setDefaultButtonTitle:TXTLS(@"Prompts[aqw-q1]")]; [panel setAlternateButtonTitle:nil]; [panel beginSheetForWindow:[NSApp mainWindow] modalDelegate:nil didEndSelector:NULL contextInfo:NULL certificates:(__bridge NSArray *)certificateArray showGroup:YES]; CFRelease(certificateArray); } /* ------ */ else if ([commandString isEqualToStringIgnoringCase:@"DETACH"] || [commandString isEqualToStringIgnoringCase:@"ATTACH"]) { messageString = messageString.trim; IRCChannel *matchedChannel = nil; if ([client stringIsChannelName:messageString]) { matchedChannel = [client findChannel:messageString]; } else { matchedChannel = mainWindow().selectedChannel; } if (matchedChannel == nil) { return; } BOOL isAttachEvent = [commandString isEqualToStringIgnoringCase:@"ATTACH"]; matchedChannel.autoJoin = isAttachEvent; if (isAttachEvent) { [client joinUnlistedChannel:matchedChannel.name]; } else { [client sendLine:[NSString stringWithFormat:@"%@ %@", commandString, matchedChannel.name]]; [client printDebugInformation:TPILocalizedString(@"BasicLanguage[0fr-kb]", matchedChannel.name) inChannel:matchedChannel]; } } } - (NSArray *)subscribedUserInputCommands { return @[@"detach", @"attach", @"znccert"]; } - (NSArray *)subscribedServerInputCommands { return @[@"privmsg"]; } - (nullable IRCMessage *)interceptBufferExtrasPlaybackModule:(IRCMessage *)input forClient:(IRCClient *)client { if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityZNCPlaybackModule] == NO) { return input; } NSString *stringIn = [input paramAt:1]; if ([stringIn hasPrefix:@"The playback buffer for ["] && [stringIn contains:@"] channels matching ["] && // This is much cleaner than regular expression... [stringIn hasSuffix:@"] has been cleared."]) { return nil; // Ignore this event } return input; } - (nullable IRCMessage *)interceptBufferExtrasZNCModule:(IRCMessage *)input forClient:(IRCClient *)client { /* Define user information */ NSMutableArray *paramsMutable = [input.params mutableCopy]; /* We have to normalize spaces for input from buffextras because as of version 1.7, there are fixed width spaces in the incoming messages that throw off parser. */ NSMutableString *stringIn = [paramsMutable[1].normalizeSpaces mutableCopy]; NSString *hostmask = stringIn.token; if (hostmask.length == 0) { return input; // Return original; bad input } IRCPrefixMutable *senderMutable = [input.sender mutableCopy]; NSString *nicknameInt = nil; NSString *usernameInt = nil; NSString *addressInt = nil; if ([hostmask hostmaskComponents:&nicknameInt username:&usernameInt address:&addressInt onClient:client]) { if ([nicknameInt isEqualToString:client.userNickname]) { return nil; // Do not post these events for self } senderMutable.nickname = nicknameInt; senderMutable.username = usernameInt; senderMutable.address = addressInt; senderMutable.isServer = NO; } else { senderMutable.nickname = hostmask; senderMutable.isServer = YES; } senderMutable.hostmask = hostmask; /* Process string */ /* The output of the buffextras module is not guaranteed. The ZNC author(s) have stated that they shouldn't be treated as such. ZNC 1.7 changed it so that the output of the module can be localized. We will ever only handle English here to reduce footprint. */ IRCMessageMutable *inputMutable = [input mutableCopy]; NSArray *stringInComponents = nil; if ([stringIn isEqualToString:@"joined"]) { /* Begin channel join */ inputMutable.command = @"JOIN"; [paramsMutable removeObjectAtIndex:1]; /* End channel join */ } else if ([XRRegularExpression matches:&stringInComponents inString:stringIn withRegex:@"^is now known as ([^\\s]+)$" withoutCase:NO substringGroups:YES] == 2) { /* Begin nickname change */ NSString *newNickname = stringInComponents[1]; inputMutable.command = @"NICK"; [paramsMutable removeObjectAtIndex:1]; [paramsMutable addObject:newNickname]; /* End nickname change */ } else if ([XRRegularExpression matches:&stringInComponents inString:stringIn withRegex:@"^(parted with message: \\[(.*)\\]|parted: (.*))$" withoutCase:NO substringGroups:YES] == 3) { /* Begin part message */ NSString *partMessage = stringInComponents[2]; inputMutable.command = @"PART"; [paramsMutable removeObjectAtIndex:1]; [paramsMutable addObject:partMessage]; /* End part message */ } else if ([XRRegularExpression matches:&stringInComponents inString:stringIn withRegex:@"^(quit with message: \\[(.*)\\]|quit: (.*))$" withoutCase:NO substringGroups:YES] == 3) { /* Begin quit message */ NSString *quitMessage = stringInComponents[2]; inputMutable.command = @"QUIT"; [paramsMutable removeObjectAtIndex:1]; [paramsMutable addObject:quitMessage]; /* End quit message */ } else if ([XRRegularExpression matches:&stringInComponents inString:stringIn withRegex:@"^(kicked ([^\\s]+) with reason: (.*)|kicked ([^\\s]+) Reason: \\[(.*)\\])$" withoutCase:NO substringGroups:YES] == 4) { /* Begin kick message */ NSString *kickedNickname = stringInComponents[2]; NSString *kickReason = stringInComponents[3]; inputMutable.command = @"KICK"; [paramsMutable removeObjectAtIndex:1]; [paramsMutable addObject:kickedNickname]; [paramsMutable addObject:kickReason]; /* End kick message. */ } else if ([XRRegularExpression matches:&stringInComponents inString:stringIn withRegex:@"^set mode: ([^\\s]+)( .*)?$" withoutCase:NO substringGroups:YES] >= 2) { /* Begin mode processing */ NSString *modeChanges = stringInComponents[1]; if (stringInComponents.count == 3) { modeChanges = [modeChanges stringByAppendingString:stringInComponents[2]]; } inputMutable.command = @"MODE"; [paramsMutable removeObjectAtIndex:1]; [paramsMutable addObject:modeChanges]; /* End mode processing */ } else if ([stringIn hasPrefix:@"changed the topic to: "]) { /* Begin topic change */ /* We get the latest topic on join so we tell Textual to ignore this line. */ return nil; /* End topic change */ } /* Return modified input */ inputMutable.isPrintOnlyMessage = YES; inputMutable.params = paramsMutable; inputMutable.sender = senderMutable; return inputMutable; } - (nullable IRCMessage *)interceptServerInput:(IRCMessage *)input for:(IRCClient *)client { if (input.paramsCount != 2) { return input; } if (client.isConnectedToZNC == NO) { return input; } if ([input.command isEqualToString:@"PRIVMSG"] == NO) { return input; } IRCPrefix *senderInfo = input.sender; NSString *sender = senderInfo.nickname; if ([client nickname:sender isZNCUser:@"buffextras"]) { return [self interceptBufferExtrasZNCModule:input forClient:client]; } else if ([client nickname:sender isZNCUser:@"playback"]) { return [self interceptBufferExtrasPlaybackModule:input forClient:client]; } return input; } #pragma mark - #pragma mark Private API - (void)handleIRCSideDisconnect:(IRCClient *)client { for (IRCChannel *channel in client.channelList) { if (channel.isActive == NO) { continue; } if ([channel.name hasPrefix:@"~#"]) { // Don't leave internal channels continue; } [channel deactivate]; } [mainWindow() reloadTreeGroup:client]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Plugins/ZNC Additions/Resources/Language Files/en.lproj/BasicLanguage.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2011 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "xex-nl" = "This command is not available because you aren't connected to ZNC"; "0fr-kb" = "The \002detach\002 command has been sent for the channel \002%@\002. Automatic join on connect for this channel has been disabled as well. Use the command \002attach\002 at any time to be returned to this channel and have automatic join turned back on."; "moh-hg" = "No information is available"; "wco-zv" = "An error occurred while converting certificate information to a friendlier format"; ================================================ FILE: Sources/Plugins/ZNC Additions/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleVersion 1.0.1 NSHumanReadableCopyright Copyright © 2011 - 2018 Codeux Software, LLC. All rights reserved. MinimumTextualVersion 7.2.4 NSPrincipalClass TPI_ZNCAdditions ================================================ FILE: Sources/Plugins/ZNC Additions/ZNC Additions Extension.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C36AAC820F27AB4007CA939 /* BasicLanguage.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C36AAC620F27AB4007CA939 /* BasicLanguage.strings */; }; 4C46A08020EC6A8200094EA4 /* TPI_ZNCAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A07F20EC6A8200094EA4 /* TPI_ZNCAdditions.m */; }; 4C661D491C2428DC001C2352 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C661D481C2428DC001C2352 /* Security.framework */; }; 4CF7B6C81C02836A00C986AA /* SecurityInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CF7B6C71C02836A00C986AA /* SecurityInterface.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C1D102820EBF29F00E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D102920EBF29F00E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D102D20EBF29F00E72D80 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C1D103B20EBF29F00E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D103C20EBF29F00E72D80 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4C1D103D20EBF29F00E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D103F20EBF29F00E72D80 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4C1D104220EBF29F00E72D80 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4C1D105020EBF29F00E72D80 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4C1D105120EBF29F00E72D80 /* Textual Extensions.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Textual Extensions.xcconfig"; sourceTree = ""; }; 4C1D105520EBF29F00E72D80 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4C36AAC720F27AB4007CA939 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/BasicLanguage.strings; sourceTree = ""; }; 4C46A07A20EC6A5000094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C46A07E20EC6A8200094EA4 /* TPI_ZNCAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPI_ZNCAdditions.h; sourceTree = ""; }; 4C46A07F20EC6A8200094EA4 /* TPI_ZNCAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPI_ZNCAdditions.m; sourceTree = ""; }; 4C661D481C2428DC001C2352 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 4CF7B6C71C02836A00C986AA /* SecurityInterface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SecurityInterface.framework; path = System/Library/Frameworks/SecurityInterface.framework; sourceTree = SDKROOT; }; 8D576316048677EA00EA77CD /* ZNC Additions.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ZNC Additions.bundle"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 8D576313048677EA00EA77CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4C661D491C2428DC001C2352 /* Security.framework in Frameworks */, 4CF7B6C81C02836A00C986AA /* SecurityInterface.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 089C166AFE841209C02AAC07 /* BragSpam */ = { isa = PBXGroup; children = ( 4C46A07D20EC6A8200094EA4 /* Classes */, 089C167CFE841241C02AAC07 /* Resources */, 089C1671FE841209C02AAC07 /* Frameworks */, 19C28FB6FE9D52B211CA2CBB /* Products */, ); name = BragSpam; sourceTree = ""; }; 089C1671FE841209C02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( 4C661D481C2428DC001C2352 /* Security.framework */, 4CF7B6C71C02836A00C986AA /* SecurityInterface.framework */, ); name = Frameworks; sourceTree = ""; }; 089C167CFE841241C02AAC07 /* Resources */ = { isa = PBXGroup; children = ( 4C69188516E3275D00BDE3A8 /* Configurations */, 4C46A07720EC6A5000094EA4 /* Language Files */, 4C46A07920EC6A5000094EA4 /* Property Lists */, ); path = Resources; sourceTree = ""; }; 19C28FB6FE9D52B211CA2CBB /* Products */ = { isa = PBXGroup; children = ( 8D576316048677EA00EA77CD /* ZNC Additions.bundle */, ); name = Products; sourceTree = ""; }; 4C1D102420EBF29F00E72D80 /* Build */ = { isa = PBXGroup; children = ( 4C1D103820EBF29F00E72D80 /* Common */, 4C1D104D20EBF29F00E72D80 /* Debug */, 4C1D102520EBF29F00E72D80 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4C1D102520EBF29F00E72D80 /* Standard Release */ = { isa = PBXGroup; children = ( 4C1D102D20EBF29F00E72D80 /* Enabled Features.xcconfig */, 4C1D102920EBF29F00E72D80 /* Textual Extensions.xcconfig */, 4C1D102820EBF29F00E72D80 /* Textual.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4C1D103820EBF29F00E72D80 /* Common */ = { isa = PBXGroup; children = ( 4C1D104220EBF29F00E72D80 /* Foundation Debug.xcconfig */, 4C1D103F20EBF29F00E72D80 /* Foundation.xcconfig */, 4C1D103C20EBF29F00E72D80 /* Preserve Symbols.xcconfig */, 4C1D103D20EBF29F00E72D80 /* Textual Extensions.xcconfig */, 4C1D103B20EBF29F00E72D80 /* Textual.xcconfig */, ); path = Common; sourceTree = ""; }; 4C1D104D20EBF29F00E72D80 /* Debug */ = { isa = PBXGroup; children = ( 4C1D105520EBF29F00E72D80 /* Enabled Features.xcconfig */, 4C1D105120EBF29F00E72D80 /* Textual Extensions.xcconfig */, 4C1D105020EBF29F00E72D80 /* Textual.xcconfig */, ); path = Debug; sourceTree = ""; }; 4C46A07720EC6A5000094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C36AAC620F27AB4007CA939 /* BasicLanguage.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C46A07920EC6A5000094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A07A20EC6A5000094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A07D20EC6A8200094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A07E20EC6A8200094EA4 /* TPI_ZNCAdditions.h */, 4C46A07F20EC6A8200094EA4 /* TPI_ZNCAdditions.m */, ); path = Classes; sourceTree = ""; }; 4C69188516E3275D00BDE3A8 /* Configurations */ = { isa = PBXGroup; children = ( 4C1D102420EBF29F00E72D80 /* Build */, ); name = Configurations; path = ../../../Configurations; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 8D57630D048677EA00EA77CD /* ZNC Additions Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "ZNC Additions Extension" */; buildPhases = ( 8D57630F048677EA00EA77CD /* Resources */, 8D576313048677EA00EA77CD /* Frameworks */, 8D576311048677EA00EA77CD /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "ZNC Additions Extension"; productInstallPath = "$(HOME)/Library/Bundles"; productName = BragSpam; productReference = 8D576316048677EA00EA77CD /* ZNC Additions.bundle */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 089C1669FE841209C02AAC07 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; TargetAttributes = { 8D57630D048677EA00EA77CD = { DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "ZNC Additions Extension" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( en, Base, ); mainGroup = 089C166AFE841209C02AAC07 /* BragSpam */; projectDirPath = ""; projectRoot = ""; targets = ( 8D57630D048677EA00EA77CD /* ZNC Additions Extension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 8D57630F048677EA00EA77CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C36AAC820F27AB4007CA939 /* BasicLanguage.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 8D576311048677EA00EA77CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A08020EC6A8200094EA4 /* TPI_ZNCAdditions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C36AAC620F27AB4007CA939 /* BasicLanguage.strings */ = { isa = PBXVariantGroup; children = ( 4C36AAC720F27AB4007CA939 /* en */, ); name = BasicLanguage.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1DEB911B08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-ZNCAdditions"; PRODUCT_NAME = "ZNC Additions"; }; name = Debug; }; 1DEB911C08733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-ZNCAdditions"; PRODUCT_NAME = "ZNC Additions"; }; name = Release; }; 1DEB911F08733D790010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C1D105120EBF29F00E72D80 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Debug; }; 1DEB912008733D790010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4C1D102920EBF29F00E72D80 /* Textual Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4C95802A1FA4204000F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { }; name = "Release (App Store)"; }; 4C95802B1FA4204000F18BC8 /* Release (App Store) */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.textual-ZNCAdditions"; PRODUCT_NAME = "ZNC Additions"; }; name = "Release (App Store)"; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1DEB911A08733D790010E9CD /* Build configuration list for PBXNativeTarget "ZNC Additions Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911B08733D790010E9CD /* Debug */, 1DEB911C08733D790010E9CD /* Release */, 4C95802B1FA4204000F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB911E08733D790010E9CD /* Build configuration list for PBXProject "ZNC Additions Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB911F08733D790010E9CD /* Debug */, 1DEB912008733D790010E9CD /* Release */, 4C95802A1FA4204000F18BC8 /* Release (App Store) */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 089C1669FE841209C02AAC07 /* Project object */; } ================================================ FILE: Sources/Shared/Headers/External Libraries/GCDAsyncSocket.h ================================================ // // GCDAsyncSocket.h // // This class is in the public domain. // Originally created by Robbie Hanson in Q3 2010. // Updated and maintained by Deusty LLC and the Apple development community. // // https://github.com/robbiehanson/CocoaAsyncSocket // #import #import #import #import #import #include // AF_INET, AF_INET6 @class GCDAsyncReadPacket; @class GCDAsyncWritePacket; @class GCDAsyncSocketPreBuffer; @protocol GCDAsyncSocketDelegate; NS_ASSUME_NONNULL_BEGIN extern NSString *const GCDAsyncSocketException; extern NSString *const GCDAsyncSocketErrorDomain; extern NSString *const GCDAsyncSocketQueueName; extern NSString *const GCDAsyncSocketThreadName; extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; #if TARGET_OS_IPHONE extern NSString *const GCDAsyncSocketUseCFStreamForTLS; #endif #define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName #define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates #define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer extern NSString *const GCDAsyncSocketSSLPeerID; extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; extern NSString *const GCDAsyncSocketSSLCipherSuites; #if !TARGET_OS_IPHONE extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; #endif #define GCDAsyncSocketLoggingContext 65535 typedef NS_ENUM(NSInteger, GCDAsyncSocketError) { GCDAsyncSocketNoError = 0, // Never used GCDAsyncSocketBadConfigError, // Invalid configuration GCDAsyncSocketBadParamError, // Invalid parameter was passed GCDAsyncSocketConnectTimeoutError, // A connect operation timed out GCDAsyncSocketReadTimeoutError, // A read operation timed out GCDAsyncSocketWriteTimeoutError, // A write operation timed out GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing GCDAsyncSocketClosedError, // The remote peer closed the connection GCDAsyncSocketOtherError, // Description provided in userInfo }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @interface GCDAsyncSocket : NSObject /** * GCDAsyncSocket uses the standard delegate paradigm, * but executes all delegate callbacks on a given delegate dispatch queue. * This allows for maximum concurrency, while at the same time providing easy thread safety. * * You MUST set a delegate AND delegate dispatch queue before attempting to * use the socket, or you will get an error. * * The socket queue is optional. * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. * If you choose to provide a socket queue, and the socket queue has a configured target queue, * then please see the discussion for the method markSocketQueueTargetQueue. * * The delegate queue and socket queue can optionally be the same. **/ - (instancetype)init; - (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; - (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; - (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq; /** * Create GCDAsyncSocket from already connect BSD socket file descriptor **/ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD socketQueue:(nullable dispatch_queue_t)sq error:(NSError**)error; + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq error:(NSError**)error; + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq error:(NSError **)error; #pragma mark Configuration @property (atomic, weak, readwrite, nullable) id delegate; #if OS_OBJECT_USE_OBJC @property (atomic, strong, readwrite, nullable) dispatch_queue_t delegateQueue; #else @property (atomic, assign, readwrite, nullable) dispatch_queue_t delegateQueue; #endif - (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; - (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; /** * If you are setting the delegate to nil within the delegate's dealloc method, * you may need to use the synchronous versions below. **/ - (void)synchronouslySetDelegate:(nullable id)delegate; - (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; - (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; /** * By default, both IPv4 and IPv6 are enabled. * * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, * and can simulataneously accept incoming connections on either protocol. * * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. * By default, the preferred protocol is IPv4, but may be configured as desired. **/ @property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; @property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; @property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; /** * When connecting to both IPv4 and IPv6 using Happy Eyeballs (RFC 6555) https://tools.ietf.org/html/rfc6555 * this is the delay between connecting to the preferred protocol and the fallback protocol. * * Defaults to 300ms. **/ @property (atomic, assign, readwrite) NSTimeInterval alternateAddressDelay; /** * User data allows you to associate arbitrary information with the socket. * This data is not used internally by socket in any way. **/ @property (atomic, strong, readwrite, nullable) id userData; /** * Whether the flag DISPATCH_TIMER_STRICT is set on internal timers, * thus negating the benefits of the "App Nap" feature of macOS. **/ @property (atomic, assign, readwrite) BOOL useStrictTimers; #pragma mark Accepting /** * Tells the socket to begin listening and accepting connections on the given port. * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, * and the socket:didAcceptNewSocket: delegate method will be invoked. * * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) **/ - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; /** * This method is the same as acceptOnPort:error: with the * additional option of specifying which interface to listen on. * * For example, you could specify that the socket should only accept connections over ethernet, * and not other interfaces such as wifi. * * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). * You may also use the special strings "localhost" or "loopback" to specify that * the socket only accept connections from the local machine. * * You can see the list of interfaces via the command line utility "ifconfig", * or programmatically via the getifaddrs() function. * * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. **/ - (BOOL)acceptOnInterface:(nullable NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; /** * Tells the socket to begin listening and accepting connections on the unix domain at the given url. * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, * and the socket:didAcceptNewSocket: delegate method will be invoked. * * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) **/ - (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; #pragma mark Connecting /** * Connects to the given host and port. * * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: * and uses the default interface, and no timeout. **/ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; /** * Connects to the given host and port with an optional timeout. * * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. **/ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Connects to the given host & port, via the optional interface, with an optional timeout. * * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). * The host may also be the special strings "localhost" or "loopback" to specify connecting * to a service on the local machine. * * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). * The interface may also be used to specify the local port (see below). * * To not time out use a negative time interval. * * This method will return NO if an error is detected, and set the error pointer (if one was given). * Possible errors would be a nil host, invalid interface, or socket is already connected. * * If no errors are detected, this method will start a background connect operation and immediately return YES. * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. * * Since this class supports queued reads and writes, you can immediately start reading and/or writing. * All read/write operations will be queued, and upon socket connection, * the operations will be dequeued and processed in order. * * The interface may optionally contain a port number at the end of the string, separated by a colon. * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". * To specify only local port: ":8082". * Please note this is an advanced feature, and is somewhat hidden on purpose. * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. * Local ports do NOT need to match remote ports. In fact, they almost never do. * This feature is here for networking professionals using very advanced techniques. **/ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port viaInterface:(nullable NSString *)interface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. * For example, a NSData object returned from NSNetService's addresses method. * * If you have an existing struct sockaddr you can convert it to a NSData object like so: * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; * * This method invokes connectToAdd **/ - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; /** * This method is the same as connectToAddress:error: with an additional timeout option. * To not time out use a negative time interval, or simply use the connectToAddress:error: method. **/ - (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Connects to the given address, using the specified interface and timeout. * * The address is specified as a sockaddr structure wrapped in a NSData object. * For example, a NSData object returned from NSNetService's addresses method. * * If you have an existing struct sockaddr you can convert it to a NSData object like so: * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; * * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). * The interface may also be used to specify the local port (see below). * * The timeout is optional. To not time out use a negative time interval. * * This method will return NO if an error is detected, and set the error pointer (if one was given). * Possible errors would be a nil host, invalid interface, or socket is already connected. * * If no errors are detected, this method will start a background connect operation and immediately return YES. * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. * * Since this class supports queued reads and writes, you can immediately start reading and/or writing. * All read/write operations will be queued, and upon socket connection, * the operations will be dequeued and processed in order. * * The interface may optionally contain a port number at the end of the string, separated by a colon. * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". * To specify only local port: ":8082". * Please note this is an advanced feature, and is somewhat hidden on purpose. * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. * Local ports do NOT need to match remote ports. In fact, they almost never do. * This feature is here for networking professionals using very advanced techniques. **/ - (BOOL)connectToAddress:(NSData *)remoteAddr viaInterface:(nullable NSString *)interface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Connects to the unix domain socket at the given url, using the specified timeout. */ - (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; #pragma mark Disconnecting /** * Disconnects immediately (synchronously). Any pending reads or writes are dropped. * * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. * * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) * [asyncSocket setDelegate:nil]; * [asyncSocket disconnect]; * [asyncSocket release]; * * If you plan on disconnecting the socket, and then immediately asking it to connect again, * you'll likely want to do so like this: * [asyncSocket setDelegate:nil]; * [asyncSocket disconnect]; * [asyncSocket setDelegate:self]; * [asyncSocket connect...]; **/ - (void)disconnect; /** * Disconnects after all pending reads have completed. * After calling this, the read and write methods will do nothing. * The socket will disconnect even if there are still pending writes. **/ - (void)disconnectAfterReading; /** * Disconnects after all pending writes have completed. * After calling this, the read and write methods will do nothing. * The socket will disconnect even if there are still pending reads. **/ - (void)disconnectAfterWriting; /** * Disconnects after all pending reads and writes have completed. * After calling this, the read and write methods will do nothing. **/ - (void)disconnectAfterReadingAndWriting; #pragma mark Diagnostics /** * Returns whether the socket is disconnected or connected. * * A disconnected socket may be recycled. * That is, it can be used again for connecting or listening. * * If a socket is in the process of connecting, it may be neither disconnected nor connected. **/ @property (atomic, readonly) BOOL isDisconnected; @property (atomic, readonly) BOOL isConnected; /** * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. * The host will be an IP address. **/ @property (atomic, readonly, nullable) NSString *connectedHost; @property (atomic, readonly) uint16_t connectedPort; @property (atomic, readonly, nullable) NSURL *connectedUrl; @property (atomic, readonly, nullable) NSString *localHost; @property (atomic, readonly) uint16_t localPort; /** * Returns the local or remote address to which this socket is connected, * specified as a sockaddr structure wrapped in a NSData object. * * @seealso connectedHost * @seealso connectedPort * @seealso localHost * @seealso localPort **/ @property (atomic, readonly, nullable) NSData *connectedAddress; @property (atomic, readonly, nullable) NSData *localAddress; /** * Returns whether the socket is IPv4 or IPv6. * An accepting socket may be both. **/ @property (atomic, readonly) BOOL isIPv4; @property (atomic, readonly) BOOL isIPv6; /** * Returns whether or not the socket has been secured via SSL/TLS. * * See also the startTLS method. **/ @property (atomic, readonly) BOOL isSecure; #pragma mark Reading // The readData and writeData methods won't block (they are asynchronous). // // When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. // When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. // // You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) // If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method // is called to optionally allow you to extend the timeout. // Upon a timeout, the "socket:didDisconnectWithError:" method is called // // The tag is for your convenience. // You can use it as an array index, step number, state id, pointer, etc. /** * Reads the first available bytes that become available on the socket. * * If the timeout value is negative, the read operation will not use a timeout. **/ - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Reads the first available bytes that become available on the socket. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer if nil, the socket will create a buffer for you. * * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing, and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while the socket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. **/ - (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag; /** * Reads the first available bytes that become available on the socket. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * A maximum of length bytes will be read. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer if nil, a buffer will automatically be created for you. * If maxLength is zero, no length restriction is enforced. * * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing, and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while the socket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. **/ - (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)length tag:(long)tag; /** * Reads the given number of bytes. * * If the timeout value is negative, the read operation will not use a timeout. * * If the length is 0, this method does nothing and the delegate is not called. **/ - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Reads the given number of bytes. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer if nil, a buffer will automatically be created for you. * * If the length is 0, this method does nothing and the delegate is not called. * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing, and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. **/ - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag; /** * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. * * If the timeout value is negative, the read operation will not use a timeout. * * If you pass nil or zero-length data as the "data" parameter, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. * If you're developing your own custom protocol, be sure your separator can not occur naturally as * part of the data between separators. * For example, imagine you want to send several small documents over a socket. * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. * In this particular example, it would be better to use a protocol similar to HTTP with * a header that includes the length of the document. * Also be careful that your separator cannot occur naturally as part of the encoding for a character. * * The given data (separator) parameter should be immutable. * For performance reasons, the socket will retain it, not copy it. * So if it is immutable, don't modify it while the socket is using it. **/ - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer if nil, a buffer will automatically be created for you. * * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while the socket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. * * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. * If you're developing your own custom protocol, be sure your separator can not occur naturally as * part of the data between separators. * For example, imagine you want to send several small documents over a socket. * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. * In this particular example, it would be better to use a protocol similar to HTTP with * a header that includes the length of the document. * Also be careful that your separator cannot occur naturally as part of the encoding for a character. * * The given data (separator) parameter should be immutable. * For performance reasons, the socket will retain it, not copy it. * So if it is immutable, don't modify it while the socket is using it. **/ - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag; /** * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. * * If the timeout value is negative, the read operation will not use a timeout. * * If maxLength is zero, no length restriction is enforced. * Otherwise if maxLength bytes are read without completing the read, * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. * * If you pass nil or zero-length data as the "data" parameter, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * If you pass a maxLength parameter that is less than the length of the data parameter, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. * If you're developing your own custom protocol, be sure your separator can not occur naturally as * part of the data between separators. * For example, imagine you want to send several small documents over a socket. * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. * In this particular example, it would be better to use a protocol similar to HTTP with * a header that includes the length of the document. * Also be careful that your separator cannot occur naturally as part of the encoding for a character. * * The given data (separator) parameter should be immutable. * For performance reasons, the socket will retain it, not copy it. * So if it is immutable, don't modify it while the socket is using it. **/ - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; /** * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer if nil, a buffer will automatically be created for you. * * If maxLength is zero, no length restriction is enforced. * Otherwise if maxLength bytes are read without completing the read, * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. * * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while the socket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. * * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. * If you're developing your own custom protocol, be sure your separator can not occur naturally as * part of the data between separators. * For example, imagine you want to send several small documents over a socket. * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. * In this particular example, it would be better to use a protocol similar to HTTP with * a header that includes the length of the document. * Also be careful that your separator cannot occur naturally as part of the encoding for a character. * * The given data (separator) parameter should be immutable. * For performance reasons, the socket will retain it, not copy it. * So if it is immutable, don't modify it while the socket is using it. **/ - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)length tag:(long)tag; /** * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. **/ - (float)progressOfReadReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; #pragma mark Writing /** * Writes data to the socket, and calls the delegate when finished. * * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. * If the timeout value is negative, the write operation will not use a timeout. * * Thread-Safety Note: * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. * This is for performance reasons. Often times, if NSMutableData is passed, it is because * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. **/ - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. **/ - (float)progressOfWriteReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; #pragma mark Security /** * Secures the connection using SSL/TLS. * * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing * the upgrade to TLS at the same time, without having to wait for the write to finish. * Any reads or writes scheduled after this method is called will occur over the secured connection. * * ==== The available TOP-LEVEL KEYS are: * * - GCDAsyncSocketManuallyEvaluateTrust * The value must be of type NSNumber, encapsulating a BOOL value. * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. * Instead it will pause at the moment evaulation would typically occur, * and allow us to handle the security evaluation however we see fit. * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. * * Note that if you set this option, then all other configuration keys are ignored. * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. * * For more information on trust evaluation see: * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation * https://developer.apple.com/library/ios/technotes/tn2232/_index.html * * If unspecified, the default value is NO. * * - GCDAsyncSocketUseCFStreamForTLS (iOS only) * The value must be of type NSNumber, encapsulating a BOOL value. * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. * This gives us more control over the security protocol (many more configuration options), * plus it allows us to optimize things like sys calls and buffer allocation. * * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. * * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. * * If unspecified, the default value is NO. * * ==== The available CONFIGURATION KEYS are: * * - kCFStreamSSLPeerName * The value must be of type NSString. * It should match the name in the X.509 certificate given by the remote party. * See Apple's documentation for SSLSetPeerDomainName. * * - kCFStreamSSLCertificates * The value must be of type NSArray. * See Apple's documentation for SSLSetCertificate. * * - kCFStreamSSLIsServer * The value must be of type NSNumber, encapsulationg a BOOL value. * See Apple's documentation for SSLCreateContext for iOS. * This is optional for iOS. If not supplied, a NO value is the default. * This is not needed for Mac OS X, and the value is ignored. * * - GCDAsyncSocketSSLPeerID * The value must be of type NSData. * You must set this value if you want to use TLS session resumption. * See Apple's documentation for SSLSetPeerID. * * - GCDAsyncSocketSSLProtocolVersionMin * - GCDAsyncSocketSSLProtocolVersionMax * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. * See also the SSLProtocol typedef. * * - GCDAsyncSocketSSLSessionOptionFalseStart * The value must be of type NSNumber, encapsulating a BOOL value. * See Apple's documentation for kSSLSessionOptionFalseStart. * * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord * The value must be of type NSNumber, encapsulating a BOOL value. * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. * * - GCDAsyncSocketSSLCipherSuites * The values must be of type NSArray. * Each item within the array must be a NSNumber, encapsulating * See Apple's documentation for SSLSetEnabledCiphers. * See also the SSLCipherSuite typedef. * * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) * The value must be of type NSData. * See Apple's documentation for SSLSetDiffieHellmanParams. * * ==== The following UNAVAILABLE KEYS are: (with throw an exception) * * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). * Corresponding deprecated method: SSLSetAllowsAnyRoot * * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). * Corresponding deprecated method: SSLSetAllowsExpiredRoots * * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). * Corresponding deprecated method: SSLSetAllowsExpiredCerts * * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). * Corresponding deprecated method: SSLSetEnableCertVerify * * - kCFStreamSSLLevel (UNAVAILABLE) * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. * Corresponding deprecated method: SSLSetProtocolVersionEnabled * * * Please refer to Apple's documentation for corresponding SSLFunctions. * * If you pass in nil or an empty dictionary, the default settings will be used. * * IMPORTANT SECURITY NOTE: * The default settings will check to make sure the remote party's certificate is signed by a * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. * However it will not verify the name on the certificate unless you * give it a name to verify against via the kCFStreamSSLPeerName key. * The security implications of this are important to understand. * Imagine you are attempting to create a secure connection to MySecureServer.com, * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. * If you simply use the default settings, and MaliciousServer.com has a valid certificate, * the default settings will not detect any problems since the certificate is valid. * To properly secure your connection in this particular scenario you * should set the kCFStreamSSLPeerName property to "MySecureServer.com". * * You can also perform additional validation in socketDidSecure. **/ - (void)startTLS:(nullable NSDictionary *)tlsSettings; #pragma mark Advanced /** * Traditionally sockets are not closed until the conversation is over. * However, it is technically possible for the remote enpoint to close its write stream. * Our socket would then be notified that there is no more data to be read, * but our socket would still be writeable and the remote endpoint could continue to receive our data. * * The argument for this confusing functionality stems from the idea that a client could shut down its * write stream after sending a request to the server, thus notifying the server there are to be no further requests. * In practice, however, this technique did little to help server developers. * * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). * * In addition to the technical challenges and confusion, many high level socket/stream API's provide * no support for dealing with the problem. If the read stream is closed, the API immediately declares the * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. * It might sound like poor design at first, but in fact it simplifies development. * * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. * Thus it actually makes sense to close the socket at this point. * And in fact this is what most networking developers want and expect to happen. * However, if you are writing a server that interacts with a plethora of clients, * you might encounter a client that uses the discouraged technique of shutting down its write stream. * If this is the case, you can set this property to NO, * and make use of the socketDidCloseReadStream delegate method. * * The default value is YES. **/ @property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; /** * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. * In most cases, the instance creates this queue itself. * However, to allow for maximum flexibility, the internal queue may be passed in the init method. * This allows for some advanced options such as controlling socket priority via target queues. * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. * * For example, imagine there are 2 queues: * dispatch_queue_t socketQueue; * dispatch_queue_t socketTargetQueue; * * If you do this (pseudo-code): * socketQueue.targetQueue = socketTargetQueue; * * Then all socketQueue operations will actually get run on the given socketTargetQueue. * This is fine and works great in most situations. * But if you run code directly from within the socketTargetQueue that accesses the socket, * you could potentially get deadlock. Imagine the following code: * * - (BOOL)socketHasSomething * { * __block BOOL result = NO; * dispatch_block_t block = ^{ * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; * } * if (is_executing_on_queue(socketQueue)) * block(); * else * dispatch_sync(socketQueue, block); * * return result; * } * * What happens if you call this method from the socketTargetQueue? The result is deadlock. * This is because the GCD API offers no mechanism to discover a queue's targetQueue. * Thus we have no idea if our socketQueue is configured with a targetQueue. * If we had this information, we could easily avoid deadlock. * But, since these API's are missing or unfeasible, you'll have to explicitly set it. * * IF you pass a socketQueue via the init method, * AND you've configured the passed socketQueue with a targetQueue, * THEN you should pass the end queue in the target hierarchy. * * For example, consider the following queue hierarchy: * socketQueue -> ipQueue -> moduleQueue * * This example demonstrates priority shaping within some server. * All incoming client connections from the same IP address are executed on the same target queue. * And all connections for a particular module are executed on the same target queue. * Thus, the priority of all networking for the entire module can be changed on the fly. * Additionally, networking traffic from a single IP cannot monopolize the module. * * Here's how you would accomplish something like that: * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock * { * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; * * dispatch_set_target_queue(socketQueue, ipQueue); * dispatch_set_target_queue(iqQueue, moduleQueue); * * return socketQueue; * } * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket * { * [clientConnections addObject:newSocket]; * [newSocket markSocketQueueTargetQueue:moduleQueue]; * } * * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. * This is often NOT the case, as such queues are used solely for execution shaping. **/ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; - (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; /** * It's not thread-safe to access certain variables from outside the socket's internal queue. * * For example, the socket file descriptor. * File descriptors are simply integers which reference an index in the per-process file table. * However, when one requests a new file descriptor (by opening a file or socket), * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. * So if we're not careful, the following could be possible: * * - Thread A invokes a method which returns the socket's file descriptor. * - The socket is closed via the socket's internal queue on thread B. * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. * - Thread A is now accessing/altering the file instead of the socket. * * In addition to this, other variables are not actually objects, * and thus cannot be retained/released or even autoreleased. * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. * * Although there are internal variables that make it difficult to maintain thread-safety, * it is important to provide access to these variables * to ensure this class can be used in a wide array of environments. * This method helps to accomplish this by invoking the current block on the socket's internal queue. * The methods below can be invoked from within the block to access * those generally thread-unsafe internal variables in a thread-safe manner. * The given block will be invoked synchronously on the socket's internal queue. * * If you save references to any protected variables and use them outside the block, you do so at your own peril. **/ - (void)performBlock:(dispatch_block_t)block; /** * These methods are only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Provides access to the socket's file descriptor(s). * If the socket is a server socket (is accepting incoming connections), * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. **/ - (int)socketFD; - (int)socket4FD; - (int)socket6FD; #if TARGET_OS_IPHONE /** * These methods are only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Provides access to the socket's internal CFReadStream/CFWriteStream. * * These streams are only used as workarounds for specific iOS shortcomings: * * - Apple has decided to keep the SecureTransport framework private is iOS. * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, * instead of the preferred and faster and more powerful SecureTransport. * * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, * Apple only bothers to notify us via the CFStream API. * The faster and more powerful GCD API isn't notified properly in this case. * * See also: (BOOL)enableBackgroundingOnSocket **/ - (nullable CFReadStreamRef)readStream; - (nullable CFWriteStreamRef)writeStream; /** * This method is only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Configures the socket to allow it to operate when the iOS application has been backgrounded. * In other words, this method creates a read & write stream, and invokes: * * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); * * Returns YES if successful, NO otherwise. * * Note: Apple does not officially support backgrounding server sockets. * That is, if your socket is accepting incoming connections, Apple does not officially support * allowing iOS applications to accept incoming connections while an app is backgrounded. * * Example usage: * * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port * { * [asyncSocket performBlock:^{ * [asyncSocket enableBackgroundingOnSocket]; * }]; * } **/ - (BOOL)enableBackgroundingOnSocket; #endif /** * This method is only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. **/ - (nullable SSLContextRef)sslContext; #pragma mark Utilities /** * The address lookup utility used by the class. * This method is synchronous, so it's recommended you use it on a background thread/queue. * * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. * * @returns * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. * The addresses are specifically for TCP connections. * You can filter the addresses, if needed, using the other utility methods provided by the class. **/ + (nullable NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; /** * Extracting host and port information from raw address data. **/ + (nullable NSString *)hostFromAddress:(NSData *)address; + (uint16_t)portFromAddress:(NSData *)address; + (BOOL)isIPv4Address:(NSData *)address; + (BOOL)isIPv6Address:(NSData *)address; + (BOOL)getHost:( NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr fromAddress:(NSData *)address; + (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr family:(nullable sa_family_t *)afPtr fromAddress:(NSData *)address; /** * A few common line separators, for use with the readDataToData:... methods. **/ + (NSData *)CRLFData; // 0x0D0A + (NSData *)CRData; // 0x0D + (NSData *)LFData; // 0x0A + (NSData *)ZeroData; // 0x00 @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @protocol GCDAsyncSocketDelegate @optional /** * This method is called immediately prior to socket:didAcceptNewSocket:. * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. * * Since you cannot autorelease a dispatch_queue, * this method uses the "new" prefix in its name to specify that the returned queue has been retained. * * Thus you could do something like this in the implementation: * return dispatch_queue_create("MyQueue", NULL); * * If you are placing multiple sockets on the same queue, * then care should be taken to increment the retain count each time this method is invoked. * * For example, your implementation might look something like this: * dispatch_retain(myExistingQueue); * return myExistingQueue; **/ - (nullable dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; /** * Called when a socket accepts a connection. * Another socket is automatically spawned to handle it. * * You must retain the newSocket if you wish to handle the connection. * Otherwise the newSocket instance will be released and the spawned connection will be closed. * * By default the new socket will have the same delegate and delegateQueue. * You may, of course, change this at any time. **/ - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; /** * Called when a socket connects and is ready for reading and writing. * The host parameter will be an IP address, not a DNS name. **/ - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; /** * Called when a socket connects and is ready for reading and writing. * The host parameter will be an IP address, not a DNS name. **/ - (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url; /** * Called when a socket has completed reading the requested data into memory. * Not called if there is an error. **/ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; /** * Called when a socket has read in data, but has not yet completed the read. * This would occur if using readToData: or readToLength: methods. * It may be used to for things such as updating progress bars. **/ - (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; /** * Called when a socket has completed writing the requested data. Not called if there is an error. **/ - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; /** * Called when a socket has written some data, but has not yet completed the entire write. * It may be used to for things such as updating progress bars. **/ - (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; /** * Called if a read operation has reached its timeout without completing. * This method allows you to optionally extend the timeout. * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. * * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. * The length parameter is the number of bytes that have been read so far for the read operation. * * Note that this method may be called multiple times for a single read if you return positive numbers. **/ - (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length; /** * Called if a write operation has reached its timeout without completing. * This method allows you to optionally extend the timeout. * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. * * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. * The length parameter is the number of bytes that have been written so far for the write operation. * * Note that this method may be called multiple times for a single write if you return positive numbers. **/ - (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length; /** * Conditionally called if the read stream closes, but the write stream may still be writeable. * * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. * See the discussion on the autoDisconnectOnClosedReadStream method for more information. **/ - (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; /** * Called when a socket disconnects with or without error. * * If you call the disconnect method, and the socket wasn't already disconnected, * then an invocation of this delegate method will be enqueued on the delegateQueue * before the disconnect method returns. * * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, * and the delegate is not also deallocated, then this method will be invoked, * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) * This is a generally rare, but is possible if one writes code like this: * * asyncSocket = nil; // I'm implicitly disconnecting the socket * * In this case it may preferrable to nil the delegate beforehand, like this: * * asyncSocket.delegate = nil; // Don't invoke my delegate method * asyncSocket = nil; // I'm implicitly disconnecting the socket * * Of course, this depends on how your state machine is configured. **/ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err; /** * Called after the socket has successfully completed SSL/TLS negotiation. * This method is not called unless you use the provided startTLS method. * * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. **/ - (void)socketDidSecure:(GCDAsyncSocket *)sock; /** * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. * * This is only called if startTLS is invoked with options that include: * - GCDAsyncSocketManuallyEvaluateTrust == YES * * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. * * Note from Apple's documentation: * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, * [it] might block while attempting network access. You should never call it from your main thread; * call it only from within a function running on a dispatch queue or on a separate thread. * * Thus this method uses a completionHandler block rather than a normal return value. * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. * It is safe to invoke the completionHandler block even if the socket has been closed. **/ - (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/External Libraries/GCDAsyncSocketExtensions.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "GCDAsyncSocket.h" NS_ASSUME_NONNULL_BEGIN @interface GCDAsyncSocket (GCDsyncSocketExtensions) + (instancetype)socketWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq; @property (readonly) tls_protocol_version_t tlsNegotiatedProtocol; @property (readonly) tls_ciphersuite_t tlsNegotiatedCipherSuite; @property (readonly) SecTrustRef tlsTrustRef; @property (readonly, copy, nullable) NSArray *tlsCertificateChainData; @property (readonly, copy, nullable) NSString *tlsPolicyName; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/IRCConnectionConfig.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #define IRCConnectionConfigFloodControlDefaultDelayInterval 2 #define IRCConnectionConfigFloodControlMinimumDelayInterval 1 #define IRCConnectionConfigFloodControlMaximumDelayInterval 60 #define IRCConnectionConfigFloodControlDefaultMessageCount 6 #define IRCConnectionConfigFloodControlMinimumMessageCount 1 #define IRCConnectionConfigFloodControlMaximumMessageCount 60 TEXTUAL_EXTERN uint16_t const IRCConnectionDefaultServerPort; TEXTUAL_EXTERN uint16_t const IRCConnectionDefaultProxyPort; typedef NS_ENUM(NSUInteger, IRCConnectionProxyType) { IRCConnectionProxyTypeNone = 0, IRCConnectionProxyTypeAutomatic = 1, IRCConnectionProxyTypeSocks4 = 4, IRCConnectionProxyTypeSocks5 = 5, IRCConnectionProxyTypeHTTP = 6, IRCConnectionProxyTypeHTTPS = 7, IRCConnectionProxyTypeTor = 8 }; /* Select specific protocol to use. This does not define preference. When a specific protocol is enabled, its counterpart is disabled. */ typedef NS_ENUM(NSUInteger, IRCConnectionAddressType) { IRCConnectionAddressTypeDefault = 0, // enable both IRCConnectionAddressTypeIPv4 NS_SWIFT_NAME(v4) = 1, IRCConnectionAddressTypeIPv6 NS_SWIFT_NAME(v6) = 2 }; #pragma mark - #pragma mark Immutable Object @interface IRCConnectionConfig : XRPortablePropertyObject @property (readonly) BOOL connectionPrefersModernCiphersOnly; @property (readonly) BOOL connectionPrefersModernSockets; @property (readonly) BOOL connectionPrefersSecuredConnection; @property (readonly) BOOL connectionShouldValidateCertificateChain; @property (readonly) IRCConnectionAddressType addressType; @property (readonly) IRCConnectionProxyType proxyType; @property (readonly) NSUInteger floodControlDelayInterval; @property (readonly) NSUInteger floodControlMaximumMessages; @property (readonly) uint16_t proxyPort; @property (readonly) uint16_t serverPort; @property (readonly, copy) NSString *serverAddress; @property (readonly, copy, nullable) NSString *proxyAddress; @property (readonly, copy, nullable) NSString *proxyPassword; @property (readonly, copy, nullable) NSString *proxyUsername; @property (readonly, copy, nullable) NSData *identityClientSideCertificate; @property (readonly) NSStringEncoding primaryEncoding NS_UNAVAILABLE; @property (readonly) NSStringEncoding fallbackEncoding NS_UNAVAILABLE; @property (readonly) RCMCipherSuiteCollection cipherSuites; @end #pragma mark - #pragma mark Mutable Object @interface IRCConnectionConfigMutable : IRCConnectionConfig @property (nonatomic, assign, readwrite) BOOL connectionPrefersModernCiphersOnly; @property (nonatomic, assign, readwrite) BOOL connectionPrefersModernSockets; @property (nonatomic, assign, readwrite) BOOL connectionPrefersSecuredConnection; @property (nonatomic, assign, readwrite) BOOL connectionShouldValidateCertificateChain; @property (nonatomic, assign, readwrite) IRCConnectionAddressType addressType; @property (nonatomic, assign, readwrite) IRCConnectionProxyType proxyType; @property (nonatomic, assign, readwrite) NSUInteger floodControlDelayInterval; @property (nonatomic, assign, readwrite) NSUInteger floodControlMaximumMessages; @property (nonatomic, assign, readwrite) uint16_t proxyPort; @property (nonatomic, assign, readwrite) uint16_t serverPort; @property (nonatomic, copy, readwrite) NSString *serverAddress; @property (nonatomic, copy, readwrite, nullable) NSString *proxyAddress; @property (nonatomic, copy, readwrite, nullable) NSString *proxyPassword; @property (nonatomic, copy, readwrite, nullable) NSString *proxyUsername; @property (nonatomic, copy, readwrite, nullable) NSData *identityClientSideCertificate; @property (nonatomic, assign, readwrite) NSStringEncoding primaryEncoding NS_UNAVAILABLE; @property (nonatomic, assign, readwrite) NSStringEncoding fallbackEncoding NS_UNAVAILABLE; @property (nonatomic, assign, readwrite) RCMCipherSuiteCollection cipherSuites; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/IRCConnectionErrors.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN extern NSErrorDomain const IRCConnectionErrorDomain NS_SWIFT_NAME(ConnectionErrorDomain); typedef NS_ENUM(NSUInteger, IRCConnectionErrorCode) { IRCConnectionErrorCodeSocket = 999, IRCConnectionErrorCodeOther = 1000, IRCConnectionErrorCodeBadCertificate = 1001, IRCConnectionErrorCodeUnableToSecure = 1002 } NS_SWIFT_NAME(ConnectionErrorCode); NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/Internal/IRCConnectionConfigInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCConnectionConfig.h" NS_ASSUME_NONNULL_BEGIN @interface IRCConnectionConfig () { @protected BOOL _connectionPrefersModernCiphersOnly; BOOL _connectionPrefersModernSockets; BOOL _connectionPrefersSecuredConnection; BOOL _connectionShouldValidateCertificateChain; IRCConnectionAddressType _addressType; IRCConnectionProxyType _proxyType; NSData *_identityClientSideCertificate; NSString *_proxyAddress; NSString *_proxyPassword; NSString *_proxyUsername; NSString *_serverAddress; NSUInteger _floodControlDelayInterval; NSUInteger _floodControlMaximumMessages; uint16_t _proxyPort; uint16_t _serverPort; NSStringEncoding _primaryEncoding; NSStringEncoding _fallbackEncoding; RCMCipherSuiteCollection _cipherSuites; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/Private/NSObjectHelperPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface NSObject (TXObjectHelper) - (void)applicationDidFinishLaunching; - (void)preferencesChanged; - (void)prepareInitialState; - (void)prepareForApplicationTermination; - (void)prepareForPermanentDestruction; + (void)preferencesChanged; + (void)prepareInitialState; + (void)prepareForApplicationTermination; + (void)prepareForPermanentDestruction; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/Private/TPCPreferencesPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferences.h" NS_ASSUME_NONNULL_BEGIN @interface TPCPreferences () + (void)setInlineMediaMaxWidth:(NSUInteger)value; + (void)setInlineMediaMaxHeight:(NSUInteger)value; + (void)setInlineMediaLimitToBasics:(BOOL)inlineMediaLimitToBasics; + (void)setInlineMediaLimitBasicsToFiles:(BOOL)inlineMediaLimitBasicsToFiles; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/Private/TPCPreferencesUserDefaultsPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferencesUserDefaults.h" NS_ASSUME_NONNULL_BEGIN @interface TPCPreferencesUserDefaults () - (void)setObject:(nullable id)value forKey:(NSString *)defaultName postNotification:(BOOL)postNotification; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/Private/TVCLogLineXPCPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import NS_ASSUME_NONNULL_BEGIN @interface TVCLogLineXPC : NSObject @property (copy, readonly) NSData *data; @property (copy, readonly) NSString *uniqueIdentifier; @property (copy, readonly) NSString *viewIdentifier; @property (readonly) NSUInteger sessionIdentifier; @property (readonly) NSTimeInterval creationDate; - (instancetype)initWithLogLineData:(NSData *)data uniqueIdentifier:(NSString *)uniqueIdentifier viewIdentifier:(NSString *)viewIdentifier sessionIdentifier:(NSUInteger)sessionIdentifier; - (instancetype)initWithManagedObject:(NSManagedObject *)managedObject; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/StaticDefinitions.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* Defines for operating system detection. */ #define TXLoadMacOSVersionSpecificFeatures 1 /* Shortcut defines. */ #define RZAnimationCurrentContext() [NSAnimationContext currentContext] #define RZAppearanceCurrentController() [NSAppearance currentAppearance] #define RZAppleEventManager() [NSAppleEventManager sharedAppleEventManager] #define RZCurrentCalendar() [NSCalendar currentCalendar] #define RZCurrentRunLoop() [NSRunLoop currentRunLoop] #define RZDistributedNotificationCenter() [NSDistributedNotificationCenter defaultCenter] #define RZFileManager() [NSFileManager defaultManager] #define RZFontManager() [NSFontManager sharedFontManager] #define RZGraphicsCurrentContext() [NSGraphicsContext currentContext] #define RZMainBundle() [NSBundle mainBundle] #define RZMainOperationQueue() [NSOperationQueue mainQueue] #define RZMainRunLoop() [NSRunLoop mainRunLoop] #define RZMainScreen() [NSScreen mainScreen] #define RZNotificationCenter() [NSNotificationCenter defaultCenter] #define RZPasteboard() [NSPasteboard generalPasteboard] #define RZProcessInfo() [NSProcessInfo processInfo] #define RZRunningApplication() [NSRunningApplication currentApplication] #define RZSharedApplication() [NSApplication sharedApplication] #define RZSpellChecker() [NSSpellChecker sharedSpellChecker] #define RZUbiquitousKeyValueStore() [NSUbiquitousKeyValueStore defaultStore] #define RZUserNotificationCenter() [UNUserNotificationCenter currentNotificationCenter] #define RZWorkspace() [NSWorkspace sharedWorkspace] #define RZWorkspaceNotificationCenter() [[NSWorkspace sharedWorkspace] notificationCenter] /* Misc. */ #define NSInvertedComparisonResult(c) ((c) * (-1)) #define NSIsCurrentThreadMain() [[NSThread isMainThread]] /* Deprecation and symbol visibility. */ #define TEXTUAL_EXTERN extern #define TEXTUAL_SYMBOL_USED __attribute__((used)) #define TEXTUAL_RUNNING_ON(version, name) COCOA_EXTENSIONS_RUNNING_ON(version, name) #define TEXTUAL_RUNNING_ON_BIGSUR TEXTUAL_RUNNING_ON(11.0, BigSur) #define TEXTUAL_RUNNING_ON_MOJAVE TEXTUAL_RUNNING_ON(10.14, Mojave) #define TEXTUAL_RUNNING_ON_HIGHSIERRA TEXTUAL_RUNNING_ON(10.13, HighSierra) #define TEXTUAL_RUNNING_ON_SIERRA TEXTUAL_RUNNING_ON(10.12, Sierra) #define TEXTUAL_RUNNING_ON_ELCAPITAN TEXTUAL_RUNNING_ON(10.11, ElCapitan) #define TEXTUAL_RUNNING_ON_YOSEMITE TEXTUAL_RUNNING_ON(10.10, Yosemite) #define TEXTUAL_RUNNING_ON_MAVERICKS TEXTUAL_RUNNING_ON(10.9, Mavericks) #define TEXTUAL_DEPRECATED(reason) COCOA_EXTENSIONS_DEPRECATED(reason) #define TEXTUAL_DEPRECATED_ASSERT COCOA_EXTENSIONS_DEPRECATED_ASSERT #define TEXTUAL_DEPRECATED_ASSERT_C COCOA_EXTENSIONS_DEPRECATED_ASSERT_C #define TEXTUAL_DEPRECATED_WARNING COCOA_EXTENSIONS_DEPRECATED_WARNING #define TEXTUAL_IGNORE_DEPRECATION_BEGIN COCOA_EXTENSIONS_IGNORE_DEPRECATION_BEGIN #define TEXTUAL_IGNORE_DEPRECATION_END COCOA_EXTENSIONS_IGNORE_DEPRECATION_END #define TEXTUAL_IGNORE_AVAILABILITY_BEGIN _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wpartial-availability\"") #define TEXTUAL_IGNORE_AVAILABILITY_END _Pragma("clang diagnostic pop") /* WebView deprecation */ /* The entirety of pre-WebKit2 code is deprecated by Apple. Textual uses a great deal of this code and if left unchecked, a large number of deprecation warnings will appear during build. It would be overwhelming to suppress every warning individually. Entire files have been marked to ignore deprecation warnings. As we cannot ignore WebKit specific deprecation warnings, this leaves the problem of unrelated deprecation warnings ignored. Modify the following flag routinely to check for unrelated deprecations. */ #define TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS 1 #if TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS == 1 #define TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN TEXTUAL_IGNORE_DEPRECATION_BEGIN #define TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END TEXTUAL_IGNORE_DEPRECATION_END #else #define TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_BEGIN #define TEXTUAL_IGNORE_WEBKIT_DEPRECATIONS_END #endif /* Helper function */ #define StringFromBOOL(value) ((value) ? @"YES" : @"NO") #define SetVariableIfNil(variable, value) \ if ((variable) == nil) { \ (variable) = (value); \ } #define SetVariableIfNilCopy(variable, value) \ SetVariableIfNil((variable), [(value) copy]) /* Define features */ #import "FeatureFlags.h" /* @end */ ================================================ FILE: Sources/Shared/Headers/TLOLocalization.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN TEXTUAL_EXTERN NSString *TXTLS(NSString *key, ...); TEXTUAL_EXTERN NSString *TXLocalizedString(NSBundle *bundle, NSString *key, va_list arguments) TEXTUAL_SYMBOL_USED; TEXTUAL_EXTERN NSString *TXLocalizedStringAlternative(NSBundle *bundle, NSString *key, ...) TEXTUAL_SYMBOL_USED; /* This function exists so that static analyzer doesn't warn certain static strings aren't localized. Some strings wont be localized because it is inappropriate (e.g. a number) */ __attribute__((annotate("returns_localized_nsstring"))) static inline NSString *TXLocalizationNotNeeded(NSString *string) { return string; } NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/TLOTimer.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @class TLOTimer; typedef void (^TLOTimerActionBlock)(TLOTimer *sender); @interface TLOTimer : NSObject @property (readonly, copy) TLOTimerActionBlock actionBlock; @property (nonatomic, strong, nullable) dispatch_queue_t queue; // Defaults to main queue. Changed ignored while active. @property (nonatomic, strong, nullable) id context; @property (readonly) NSTimeInterval startTime; @property (readonly) NSTimeInterval timeRemaining; @property (readonly) NSTimeInterval interval; @property (readonly) BOOL timerIsActive; @property (readonly) BOOL repeatTimer; @property (readonly) NSUInteger iterations; @property (readonly) NSUInteger currentIteration; + (instancetype)timerWithActionBlock:(TLOTimerActionBlock)actionBlock; + (instancetype)timerWithActionBlock:(TLOTimerActionBlock)actionBlock onQueue:(dispatch_queue_t)queue; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithActionBlock:(TLOTimerActionBlock)actionBlock; - (instancetype)initWithActionBlock:(TLOTimerActionBlock)actionBlock onQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; - (void)start:(NSTimeInterval)interval; // repeatTimer = NO - (void)start:(NSTimeInterval)timerInterval onRepeat:(BOOL)repeatTimer; // iterations = 0 - (void)start:(NSTimeInterval)timerInterval onRepeat:(BOOL)repeatTimer iterations:(NSUInteger)iterations; // 0 iterations = infinite - (void)stop; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/TPCPreferences.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface TPCPreferences : NSObject + (uint64_t)inlineImagesMaxFilesize; + (NSUInteger)inlineMediaMaxWidth; + (NSUInteger)inlineMediaMaxHeight; + (BOOL)inlineMediaLimitToBasics; // images & videos + (BOOL)inlineMediaLimitBasicsToFiles; // Only hotlinked files and not services + (BOOL)inlineMediaLimitInsecureContent; // No HTTP + (BOOL)inlineMediaLimitNaughtyContent; // No NSFW + (BOOL)inlineMediaLimitUnsafeContent; // No content that injects HTML // Check every single URL to find out if it is an image or video + (BOOL)inlineMediaCheckEverything; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Headers/TPCPreferencesUserDefaults.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #define RZUserDefaults() [TPCPreferencesUserDefaults sharedUserDefaults] /* The user info dictionary of this notification contains the changed key. */ TEXTUAL_EXTERN NSNotificationName const TPCPreferencesUserDefaultsDidChangeNotification; /* TPCPreferencesUserDefaults subclasses NSUserDefaults to allow Textual to fire off notifications for changed keys on a per-key basis so that the iCloud controller can know what keys change instead of having to sync every single key, every time that it performs an upstream sync. */ @interface TPCPreferencesUserDefaults : NSUserDefaults + (TPCPreferencesUserDefaults *)sharedUserDefaults; - (void)registerDefault:(id )value forKey:(NSString *)defaultName; @property (copy, readonly) NSDictionary *registeredDefaults; @end /* Trying to create a new instance of TPCPreferencesUserDefaultsController will return the value of +sharedUserDefaultsController */ @interface TPCPreferencesUserDefaultsController : NSUserDefaultsController @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Helpers/Cocoa (Objective-C)/NSObjectHelper.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @implementation NSObject (TXObjectHelper) - (void)applicationDidFinishLaunching { [self doesNotRecognizeSelector:_cmd]; } - (void)preferencesChanged { [self doesNotRecognizeSelector:_cmd]; } - (void)prepareInitialState { [self doesNotRecognizeSelector:_cmd]; } - (void)prepareForApplicationTermination { [self doesNotRecognizeSelector:_cmd]; } - (void)prepareForPermanentDestruction { [self doesNotRecognizeSelector:_cmd]; } + (void)preferencesChanged { [self doesNotRecognizeSelector:_cmd]; } + (void)prepareInitialState { [self doesNotRecognizeSelector:_cmd]; } + (void)prepareForApplicationTermination { [self doesNotRecognizeSelector:_cmd]; } + (void)prepareForPermanentDestruction { [self doesNotRecognizeSelector:_cmd]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/IRC/IRCConnectionConfig.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCConnectionConfigInternal.h" NS_ASSUME_NONNULL_BEGIN uint16_t const IRCConnectionDefaultServerPort = 6667; uint16_t const IRCConnectionDefaultProxyPort = 1080; @implementation IRCConnectionConfig - (instancetype)init { if ((self = [super init])) { [self populateDefaultsPostflight]; return self; } return nil; } - (void)populateDefaultsPostflight { if (self->_serverAddress == nil) { self->_serverAddress = @""; } if (self->_proxyPort == 0) { self->_proxyPort = IRCConnectionDefaultProxyPort; } if (self->_serverPort == 0) { self->_serverPort = IRCConnectionDefaultServerPort; } if (self->_floodControlDelayInterval == 0) { self->_floodControlDelayInterval = IRCConnectionConfigFloodControlDefaultDelayInterval; } if (self->_floodControlMaximumMessages == 0) { self->_floodControlMaximumMessages = IRCConnectionConfigFloodControlDefaultMessageCount; } } - (BOOL)populateWithDecoder:(NSCoder *)aDecoder { self->_addressType = [aDecoder decodeUnsignedIntegerForKey:@"addressType"]; self->_connectionPrefersModernCiphersOnly = [aDecoder decodeBoolForKey:@"connectionPrefersModernCiphersOnly"]; self->_connectionPrefersModernSockets = [aDecoder decodeBoolForKey:@"connectionPrefersModernSockets"]; self->_connectionPrefersSecuredConnection = [aDecoder decodeBoolForKey:@"connectionPrefersSecuredConnection"]; self->_connectionShouldValidateCertificateChain = [aDecoder decodeBoolForKey:@"connectionShouldValidateCertificateChain"]; self->_floodControlDelayInterval = [aDecoder decodeUnsignedIntegerForKey:@"floodControlDelayInterval"]; self->_floodControlMaximumMessages = [aDecoder decodeUnsignedIntegerForKey:@"floodControlMaximumMessages"]; self->_identityClientSideCertificate = [aDecoder decodeDataForKey:@"identityClientSideCertificate"]; self->_proxyAddress = [aDecoder decodeStringForKey:@"proxyAddress"]; self->_proxyPassword = [aDecoder decodeStringForKey:@"proxyPassword"]; self->_proxyPort = [aDecoder decodeUnsignedShortForKey:@"proxyPort"]; self->_proxyType = [aDecoder decodeUnsignedIntegerForKey:@"proxyType"]; self->_proxyUsername = [aDecoder decodeStringForKey:@"proxyUsername"]; self->_serverAddress = [aDecoder decodeStringForKey:@"serverAddress"]; self->_serverPort = [aDecoder decodeUnsignedShortForKey:@"serverPort"]; self->_primaryEncoding = [aDecoder decodeUnsignedIntegerForKey:@"primaryEncoding"]; self->_fallbackEncoding = [aDecoder decodeUnsignedIntegerForKey:@"fallbackEncoding"]; self->_cipherSuites = [aDecoder decodeUnsignedIntegerForKey:@"cipherSuites"]; return YES; } - (void)encodeWithCoder:(NSCoder *)aCoder { NSParameterAssert(aCoder != nil); [aCoder encodeUnsignedInteger:self->_addressType forKey:@"addressType"]; [aCoder encodeBool:self->_connectionPrefersModernCiphersOnly forKey:@"connectionPrefersModernCiphersOnly"]; [aCoder encodeBool:self->_connectionPrefersModernSockets forKey:@"connectionPrefersModernSockets"]; [aCoder encodeBool:self->_connectionPrefersSecuredConnection forKey:@"connectionPrefersSecuredConnection"]; [aCoder encodeBool:self->_connectionShouldValidateCertificateChain forKey:@"connectionShouldValidateCertificateChain"]; [aCoder encodeUnsignedInteger:self->_floodControlDelayInterval forKey:@"floodControlDelayInterval"]; [aCoder encodeUnsignedInteger:self->_floodControlMaximumMessages forKey:@"floodControlMaximumMessages"]; [aCoder maybeEncodeObject:self->_identityClientSideCertificate forKey:@"identityClientSideCertificate"]; [aCoder maybeEncodeObject:self->_proxyAddress forKey:@"proxyAddress"]; [aCoder maybeEncodeObject:self->_proxyPassword forKey:@"proxyPassword"]; [aCoder encodeUnsignedShort:self->_proxyPort forKey:@"proxyPort"]; [aCoder encodeUnsignedInteger:self->_proxyType forKey:@"proxyType"]; [aCoder maybeEncodeObject:self->_proxyUsername forKey:@"proxyUsername"]; [aCoder encodeString:self->_serverAddress forKey:@"serverAddress"]; [aCoder encodeUnsignedShort:self->_serverPort forKey:@"serverPort"]; [aCoder encodeUnsignedInteger:self->_primaryEncoding forKey:@"primaryEncoding"]; [aCoder encodeUnsignedInteger:self->_fallbackEncoding forKey:@"fallbackEncoding"]; [aCoder encodeUnsignedInteger:self->_cipherSuites forKey:@"cipherSuites"]; } + (BOOL)supportsSecureCoding { return YES; } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { IRCConnectionConfig *object = [self allocForCopyAsMutable:mutableCopy]; object->_addressType = self->_addressType; object->_connectionPrefersModernCiphersOnly = self->_connectionPrefersModernCiphersOnly; object->_connectionPrefersModernSockets = self->_connectionPrefersModernSockets; object->_connectionPrefersSecuredConnection = self->_connectionPrefersSecuredConnection; object->_connectionShouldValidateCertificateChain = self->_connectionShouldValidateCertificateChain; object->_floodControlDelayInterval = self->_floodControlDelayInterval; object->_floodControlMaximumMessages = self->_floodControlMaximumMessages; object->_identityClientSideCertificate = self->_identityClientSideCertificate; object->_proxyAddress = self->_proxyAddress; object->_proxyPassword = self->_proxyPassword; object->_proxyPort = self->_proxyPort; object->_proxyType = self->_proxyType; object->_proxyUsername = self->_proxyUsername; object->_serverAddress = self->_serverAddress; object->_serverPort = self->_serverPort; object->_primaryEncoding = self->_primaryEncoding; object->_fallbackEncoding = self->_fallbackEncoding; object->_cipherSuites = self->_cipherSuites; return [object initOnCopy]; } - (__kindof XRPortablePropertyObject *)mutableClass { return [IRCConnectionConfigMutable self]; } @end #pragma mark - @implementation IRCConnectionConfigMutable @dynamic addressType; @dynamic connectionPrefersModernCiphersOnly; @dynamic connectionPrefersModernSockets; @dynamic connectionPrefersSecuredConnection; @dynamic connectionShouldValidateCertificateChain; @dynamic floodControlDelayInterval; @dynamic floodControlMaximumMessages; @dynamic identityClientSideCertificate; @dynamic proxyAddress; @dynamic proxyPassword; @dynamic proxyPort; @dynamic proxyType; @dynamic proxyUsername; @dynamic serverAddress; @dynamic serverPort; @dynamic primaryEncoding; @dynamic fallbackEncoding; @dynamic cipherSuites; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [IRCConnectionConfig self]; } - (void)setConnectionPrefersModernCiphersOnly:(BOOL)connectionPrefersModernCiphersOnly { if (self->_connectionPrefersModernCiphersOnly != connectionPrefersModernCiphersOnly) { self->_connectionPrefersModernCiphersOnly = connectionPrefersModernCiphersOnly; } } - (void)setConnectionPrefersModernSockets:(BOOL)connectionPrefersModernSockets { if (self->_connectionPrefersModernSockets != connectionPrefersModernSockets) { self->_connectionPrefersModernSockets = connectionPrefersModernSockets; } } - (void)setConnectionPrefersSecuredConnection:(BOOL)connectionPrefersSecuredConnection { if (self->_connectionPrefersSecuredConnection != connectionPrefersSecuredConnection) { self->_connectionPrefersSecuredConnection = connectionPrefersSecuredConnection; } } - (void)setConnectionShouldValidateCertificateChain:(BOOL)connectionShouldValidateCertificateChain { if (self->_connectionShouldValidateCertificateChain != connectionShouldValidateCertificateChain) { self->_connectionShouldValidateCertificateChain = connectionShouldValidateCertificateChain; } } - (void)setAddressType:(IRCConnectionAddressType)addressType { if (self->_addressType != addressType) { self->_addressType = addressType; } } - (void)setProxyType:(IRCConnectionProxyType)proxyType { if (self->_proxyType != proxyType) { self->_proxyType = proxyType; } } - (void)setIdentityClientSideCertificate:(nullable NSData *)identityClientSideCertificate { if (self->_identityClientSideCertificate != identityClientSideCertificate) { self->_identityClientSideCertificate = identityClientSideCertificate.copy; } } - (void)setProxyAddress:(nullable NSString *)proxyAddress { if (self->_proxyAddress != proxyAddress) { self->_proxyAddress = proxyAddress.copy; } } - (void)setProxyPassword:(nullable NSString *)proxyPassword { if (self->_proxyPassword != proxyPassword) { self->_proxyPassword = proxyPassword.copy; } } - (void)setProxyUsername:(nullable NSString *)proxyUsername { if (self->_proxyUsername != proxyUsername) { self->_proxyUsername = proxyUsername.copy; } } - (void)setServerAddress:(NSString *)serverAddress { NSParameterAssert(serverAddress != nil); if (self->_serverAddress != serverAddress) { self->_serverAddress = serverAddress.copy; } } - (void)setFloodControlDelayInterval:(NSUInteger)floodControlDelayInterval { NSParameterAssert(floodControlDelayInterval >= IRCConnectionConfigFloodControlMinimumDelayInterval && floodControlDelayInterval <= IRCConnectionConfigFloodControlMaximumDelayInterval); if (self->_floodControlDelayInterval != floodControlDelayInterval) { self->_floodControlDelayInterval = floodControlDelayInterval; } } - (void)setFloodControlMaximumMessages:(NSUInteger)floodControlMaximumMessages { NSParameterAssert(floodControlMaximumMessages >= IRCConnectionConfigFloodControlMinimumMessageCount && floodControlMaximumMessages <= IRCConnectionConfigFloodControlMaximumMessageCount); if (self->_floodControlMaximumMessages != floodControlMaximumMessages) { self->_floodControlMaximumMessages = floodControlMaximumMessages; } } - (void)setProxyPort:(uint16_t)proxyPort { if (self->_proxyPort != proxyPort) { self->_proxyPort = proxyPort; } } - (void)setServerPort:(uint16_t)serverPort { if (self->_serverPort != serverPort) { self->_serverPort = serverPort; } } - (void)setPrimaryEncoding:(NSStringEncoding)primaryEncoding { if (self->_primaryEncoding != primaryEncoding) { self->_primaryEncoding = primaryEncoding; } } - (void)setFallbackEncoding:(NSStringEncoding)fallbackEncoding { if (self->_fallbackEncoding != fallbackEncoding) { self->_fallbackEncoding = fallbackEncoding; } } - (void)setCipherSuites:(RCMCipherSuiteCollection)cipherSuites { if (self->_cipherSuites != cipherSuites) { self->_cipherSuites = cipherSuites; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/IRC/IRCConnectionErrors.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "IRCConnectionErrors.h" NS_ASSUME_NONNULL_BEGIN NSString * const IRCConnectionErrorDomain = @"Textual.ConnectionError"; NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Library/External Libraries/Sockets/GCDAsyncSocket-patch.txt ================================================ commit c0e472e058df72293d9a313dc1b0d8437f3af4ae Author: Michael Morris Date: Tue Jul 5 21:21:13 2016 -0400 Patches for Textual diff --git a/Source/GCD/GCDAsyncSocket.m b/Source/GCD/GCDAsyncSocket.m old mode 100644 new mode 100755 index 1f13e36..c826db8 --- a/Source/GCD/GCDAsyncSocket.m +++ b/Source/GCD/GCDAsyncSocket.m @@ -29,6 +29,8 @@ #import #import +#define GCDAsyncSocketUsesStrictTimers 1 + #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). // For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC @@ -508,7 +510,7 @@ - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable { // Read a specific length of data - return MIN(bytesAvailable, (readLength - bytesDone)); + return (readLength - bytesDone); // No need to avoid resizing the buffer. // If the user provided their own buffer, @@ -3010,7 +3012,11 @@ - (void)startConnectTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { +#if GCDAsyncSocketUsesStrictTimers == 1 + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, socketQueue); +#else connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); +#endif __weak GCDAsyncSocket *weakSelf = self; @@ -5664,8 +5670,12 @@ - (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { +#if GCDAsyncSocketUsesStrictTimers == 1 + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, socketQueue); +#else readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - +#endif + __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { @@ -6307,8 +6317,12 @@ - (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { +#if GCDAsyncSocketUsesStrictTimers == 1 + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, socketQueue); +#else writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - +#endif + __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { @@ -7011,7 +7025,7 @@ - (void)ssl_startTLS for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) { NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; - ciphers[cipherIndex] = [cipherObject shortValue]; + ciphers[cipherIndex] = [cipherObject intValue]; } status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); ================================================ FILE: Sources/Shared/Library/External Libraries/Sockets/GCDAsyncSocket.m ================================================ // // GCDAsyncSocket.m // // This class is in the public domain. // Originally created by Robbie Hanson in Q4 2010. // Updated and maintained by Deusty LLC and the Apple development community. // // https://github.com/robbiehanson/CocoaAsyncSocket // #import "GCDAsyncSocket.h" #if TARGET_OS_IPHONE #import #endif #import #import #import #import #import #import #import #import #import #import #import #import #import #import #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). // For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC #endif #define GCDAsyncSocketReadEOFPollsSocketForWriteStatus 1 #ifndef GCDAsyncSocketLoggingEnabled #define GCDAsyncSocketLoggingEnabled 0 #endif #if GCDAsyncSocketLoggingEnabled // Logging Enabled - See log level below // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack // // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" #define LogAsync YES #define LogContext GCDAsyncSocketLoggingContext #define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) #define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) #ifndef GCDAsyncSocketLogLevel #define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE #endif // Log levels : off, error, warn, info, verbose static const int logLevel = GCDAsyncSocketLogLevel; #else // Logging Disabled #define LogError(frmt, ...) {} #define LogWarn(frmt, ...) {} #define LogInfo(frmt, ...) {} #define LogVerbose(frmt, ...) {} #define LogCError(frmt, ...) {} #define LogCWarn(frmt, ...) {} #define LogCInfo(frmt, ...) {} #define LogCVerbose(frmt, ...) {} #define LogTrace() {} #define LogCTrace(frmt, ...) {} #endif /** * Seeing a return statements within an inner block * can sometimes be mistaken for a return point of the enclosing method. * This makes inline blocks a bit easier to read. **/ #define return_from_block return /** * A socket file descriptor is really just an integer. * It represents the index of the socket within the kernel. * This makes invalid file descriptor comparisons easier to read. **/ #define SOCKET_NULL -1 NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; NSString *const GCDAsyncSocketManuallyEvaluateTrust = @"GCDAsyncSocketManuallyEvaluateTrust"; #if TARGET_OS_IPHONE NSString *const GCDAsyncSocketUseCFStreamForTLS = @"GCDAsyncSocketUseCFStreamForTLS"; #endif NSString *const GCDAsyncSocketSSLPeerID = @"GCDAsyncSocketSSLPeerID"; NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; NSString *const GCDAsyncSocketSSLSessionOptionFalseStart = @"GCDAsyncSocketSSLSessionOptionFalseStart"; NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord = @"GCDAsyncSocketSSLSessionOptionSendOneByteRecord"; NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; #if !TARGET_OS_IPHONE NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; #endif enum GCDAsyncSocketFlags { kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) kConnected = 1 << 1, // If set, the socket is connected kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. kReadSourceSuspended = 1 << 8, // If set, the read source is suspended kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained kDealloc = 1 << 16, // If set, the socket is being deallocated #if TARGET_OS_IPHONE kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available #endif }; enum GCDAsyncSocketConfig { kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes kUseStrictTimers = 1 << 4, }; #if TARGET_OS_IPHONE static NSThread *cfstreamThread; // Used for CFStreams static uint64_t cfstreamThreadRetainCount; // setup & teardown static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * A PreBuffer is used when there is more data available on the socket * than is being requested by current read request. * In this case we slurp up all data from the socket (to minimize sys calls), * and store additional yet unread data in a "prebuffer". * * The prebuffer is entirely drained before we read from the socket again. * In other words, a large chunk of data is written is written to the prebuffer. * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). * * A ring buffer was once used for this purpose. * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. * * The current design is very simple and straight-forward, while also keeping memory requirements lower. **/ @interface GCDAsyncSocketPreBuffer : NSObject { uint8_t *preBuffer; size_t preBufferSize; uint8_t *readPointer; uint8_t *writePointer; } - (id)initWithCapacity:(size_t)numBytes; - (void)ensureCapacityForWrite:(size_t)numBytes; - (size_t)availableBytes; - (uint8_t *)readBuffer; - (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; - (size_t)availableSpace; - (uint8_t *)writeBuffer; - (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; - (void)didRead:(size_t)bytesRead; - (void)didWrite:(size_t)bytesWritten; - (void)reset; @end @implementation GCDAsyncSocketPreBuffer - (id)initWithCapacity:(size_t)numBytes { if ((self = [super init])) { preBufferSize = numBytes; preBuffer = malloc(preBufferSize); readPointer = preBuffer; writePointer = preBuffer; } return self; } - (void)dealloc { if (preBuffer) free(preBuffer); } - (void)ensureCapacityForWrite:(size_t)numBytes { size_t availableSpace = [self availableSpace]; if (numBytes > availableSpace) { size_t additionalBytes = numBytes - availableSpace; size_t newPreBufferSize = preBufferSize + additionalBytes; uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); size_t readPointerOffset = readPointer - preBuffer; size_t writePointerOffset = writePointer - preBuffer; preBuffer = newPreBuffer; preBufferSize = newPreBufferSize; readPointer = preBuffer + readPointerOffset; writePointer = preBuffer + writePointerOffset; } } - (size_t)availableBytes { return writePointer - readPointer; } - (uint8_t *)readBuffer { return readPointer; } - (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr { if (bufferPtr) *bufferPtr = readPointer; if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; } - (void)didRead:(size_t)bytesRead { readPointer += bytesRead; if (readPointer == writePointer) { // The prebuffer has been drained. Reset pointers. readPointer = preBuffer; writePointer = preBuffer; } } - (size_t)availableSpace { return preBufferSize - (writePointer - preBuffer); } - (uint8_t *)writeBuffer { return writePointer; } - (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr { if (bufferPtr) *bufferPtr = writePointer; if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; } - (void)didWrite:(size_t)bytesWritten { writePointer += bytesWritten; } - (void)reset { readPointer = preBuffer; writePointer = preBuffer; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The GCDAsyncReadPacket encompasses the instructions for any given read. * The content of a read packet allows the code to determine if we're: * - reading to a certain length * - reading to a certain separator * - or simply reading the first chunk of available data **/ @interface GCDAsyncReadPacket : NSObject { @public NSMutableData *buffer; NSUInteger startOffset; NSUInteger bytesDone; NSUInteger maxLength; NSTimeInterval timeout; NSUInteger readLength; NSData *term; BOOL bufferOwner; NSUInteger originalBufferLength; long tag; } - (id)initWithData:(NSMutableData *)d startOffset:(NSUInteger)s maxLength:(NSUInteger)m timeout:(NSTimeInterval)t readLength:(NSUInteger)l terminator:(NSData *)e tag:(long)i; - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; @end @implementation GCDAsyncReadPacket - (id)initWithData:(NSMutableData *)d startOffset:(NSUInteger)s maxLength:(NSUInteger)m timeout:(NSTimeInterval)t readLength:(NSUInteger)l terminator:(NSData *)e tag:(long)i { if((self = [super init])) { bytesDone = 0; maxLength = m; timeout = t; readLength = l; term = [e copy]; tag = i; if (d) { buffer = d; startOffset = s; bufferOwner = NO; originalBufferLength = [d length]; } else { if (readLength > 0) buffer = [[NSMutableData alloc] initWithLength:readLength]; else buffer = [[NSMutableData alloc] initWithLength:0]; startOffset = 0; bufferOwner = YES; originalBufferLength = 0; } } return self; } /** * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. **/ - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; NSUInteger buffSpace = buffSize - buffUsed; if (bytesToRead > buffSpace) { NSUInteger buffInc = bytesToRead - buffSpace; [buffer increaseLengthBy:buffInc]; } } /** * This method is used when we do NOT know how much data is available to be read from the socket. * This method returns the default value unless it exceeds the specified readLength or maxLength. * * Furthermore, the shouldPreBuffer decision is based upon the packet type, * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. **/ - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr { NSUInteger result; if (readLength > 0) { // Read a specific length of data result = MIN(defaultValue, (readLength - bytesDone)); // There is no need to prebuffer since we know exactly how much data we need to read. // Even if the buffer isn't currently big enough to fit this amount of data, // it would have to be resized eventually anyway. if (shouldPreBufferPtr) *shouldPreBufferPtr = NO; } else { // Either reading until we find a specified terminator, // or we're simply reading all available data. // // In other words, one of: // // - readDataToData packet // - readDataWithTimeout packet if (maxLength > 0) result = MIN(defaultValue, (maxLength - bytesDone)); else result = defaultValue; // Since we don't know the size of the read in advance, // the shouldPreBuffer decision is based upon whether the returned value would fit // in the current buffer without requiring a resize of the buffer. // // This is because, in all likelyhood, the amount read from the socket will be less than the default value. // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. if (shouldPreBufferPtr) { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; NSUInteger buffSpace = buffSize - buffUsed; if (buffSpace >= result) *shouldPreBufferPtr = NO; else *shouldPreBufferPtr = YES; } } return result; } /** * For read packets without a set terminator, returns the amount of data * that can be read without exceeding the readLength or maxLength. * * The given parameter indicates the number of bytes estimated to be available on the socket, * which is taken into consideration during the calculation. * * The given hint MUST be greater than zero. **/ - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable { NSAssert(term == nil, @"This method does not apply to term reads"); NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); if (readLength > 0) { // Read a specific length of data return (readLength - bytesDone); // No need to avoid resizing the buffer. // If the user provided their own buffer, // and told us to read a certain length of data that exceeds the size of the buffer, // then it is clear that our code will resize the buffer during the read operation. // // This method does not actually do any resizing. // The resizing will happen elsewhere if needed. } else { // Read all available data NSUInteger result = bytesAvailable; if (maxLength > 0) { result = MIN(result, (maxLength - bytesDone)); } // No need to avoid resizing the buffer. // If the user provided their own buffer, // and told us to read all available data without giving us a maxLength, // then it is clear that our code might resize the buffer during the read operation. // // This method does not actually do any resizing. // The resizing will happen elsewhere if needed. return result; } } /** * For read packets with a set terminator, returns the amount of data * that can be read without exceeding the maxLength. * * The given parameter indicates the number of bytes estimated to be available on the socket, * which is taken into consideration during the calculation. * * To optimize memory allocations, mem copies, and mem moves * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, * or if the data can be read directly into the read packet's buffer. **/ - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr { NSAssert(term != nil, @"This method does not apply to non-term reads"); NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); NSUInteger result = bytesAvailable; if (maxLength > 0) { result = MIN(result, (maxLength - bytesDone)); } // Should the data be read into the read packet's buffer, or into a pre-buffer first? // // One would imagine the preferred option is the faster one. // So which one is faster? // // Reading directly into the packet's buffer requires: // 1. Possibly resizing packet buffer (malloc/realloc) // 2. Filling buffer (read) // 3. Searching for term (memcmp) // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) // // Reading into prebuffer first: // 1. Possibly resizing prebuffer (malloc/realloc) // 2. Filling buffer (read) // 3. Searching for term (memcmp) // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) // 5. Removing underflow from prebuffer (memmove) // // Comparing the performance of the two we can see that reading // data into the prebuffer first is slower due to the extra memove. // // However: // The implementation of NSMutableData is open source via core foundation's CFMutableData. // Decreasing the length of a mutable data object doesn't cause a realloc. // In other words, the capacity of a mutable data object can grow, but doesn't shrink. // // This means the prebuffer will rarely need a realloc. // The packet buffer, on the other hand, may often need a realloc. // This is especially true if we are the buffer owner. // Furthermore, if we are constantly realloc'ing the packet buffer, // and then moving the overflow into the prebuffer, // then we're consistently over-allocating memory for each term read. // And now we get into a bit of a tradeoff between speed and memory utilization. // // The end result is that the two perform very similarly. // And we can answer the original question very simply by another means. // // If we can read all the data directly into the packet's buffer without resizing it first, // then we do so. Otherwise we use the prebuffer. if (shouldPreBufferPtr) { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; if ((buffSize - buffUsed) >= result) *shouldPreBufferPtr = NO; else *shouldPreBufferPtr = YES; } return result; } /** * For read packets with a set terminator, * returns the amount of data that can be read from the given preBuffer, * without going over a terminator or the maxLength. * * It is assumed the terminator has not already been read. **/ - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr { NSAssert(term != nil, @"This method does not apply to non-term reads"); NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); // We know that the terminator, as a whole, doesn't exist in our own buffer. // But it is possible that a _portion_ of it exists in our buffer. // So we're going to look for the terminator starting with a portion of our own buffer. // // Example: // // term length = 3 bytes // bytesDone = 5 bytes // preBuffer length = 5 bytes // // If we append the preBuffer to our buffer, // it would look like this: // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // --------------------- // // So we start our search here: // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // -------^-^-^--------- // // And move forwards... // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // ---------^-^-^------- // // Until we find the terminator or reach the end. // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // ---------------^-^-^- BOOL found = NO; NSUInteger termLength = [term length]; NSUInteger preBufferLength = [preBuffer availableBytes]; if ((bytesDone + preBufferLength) < termLength) { // Not enough data for a full term sequence yet return preBufferLength; } NSUInteger maxPreBufferLength; if (maxLength > 0) { maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); // Note: maxLength >= termLength } else { maxPreBufferLength = preBufferLength; } uint8_t seq[termLength]; const void *termBuf = [term bytes]; NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; NSUInteger preLen = termLength - bufLen; const uint8_t *pre = [preBuffer readBuffer]; NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. NSUInteger result = maxPreBufferLength; NSUInteger i; for (i = 0; i < loopCount; i++) { if (bufLen > 0) { // Combining bytes from buffer and preBuffer memcpy(seq, buf, bufLen); memcpy(seq + bufLen, pre, preLen); if (memcmp(seq, termBuf, termLength) == 0) { result = preLen; found = YES; break; } buf++; bufLen--; preLen++; } else { // Comparing directly from preBuffer if (memcmp(pre, termBuf, termLength) == 0) { NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic result = preOffset + termLength; found = YES; break; } pre++; } } // There is no need to avoid resizing the buffer in this particular situation. if (foundPtr) *foundPtr = found; return result; } /** * For read packets with a set terminator, scans the packet buffer for the term. * It is assumed the terminator had not been fully read prior to the new bytes. * * If the term is found, the number of excess bytes after the term are returned. * If the term is not found, this method will return -1. * * Note: A return value of zero means the term was found at the very end. * * Prerequisites: * The given number of bytes have been added to the end of our buffer. * Our bytesDone variable has NOT been changed due to the prebuffered bytes. **/ - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes { NSAssert(term != nil, @"This method does not apply to non-term reads"); // The implementation of this method is very similar to the above method. // See the above method for a discussion of the algorithm used here. uint8_t *buff = [buffer mutableBytes]; NSUInteger buffLength = bytesDone + numBytes; const void *termBuff = [term bytes]; NSUInteger termLength = [term length]; // Note: We are dealing with unsigned integers, // so make sure the math doesn't go below zero. NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; while (i + termLength <= buffLength) { uint8_t *subBuffer = buff + startOffset + i; if (memcmp(subBuffer, termBuff, termLength) == 0) { return buffLength - (i + termLength); } i++; } return -1; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The GCDAsyncWritePacket encompasses the instructions for any given write. **/ @interface GCDAsyncWritePacket : NSObject { @public NSData *buffer; NSUInteger bytesDone; long tag; NSTimeInterval timeout; } - (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; @end @implementation GCDAsyncWritePacket - (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i { if((self = [super init])) { buffer = d; // Retain not copy. For performance as documented in header file. bytesDone = 0; timeout = t; tag = i; } return self; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. * This class my be altered to support more than just TLS in the future. **/ @interface GCDAsyncSpecialPacket : NSObject { @public NSDictionary *tlsSettings; } - (id)initWithTLSSettings:(NSDictionary *)settings; @end @implementation GCDAsyncSpecialPacket - (id)initWithTLSSettings:(NSDictionary *)settings { if((self = [super init])) { tlsSettings = [settings copy]; } return self; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation GCDAsyncSocket { uint32_t flags; uint16_t config; __weak id delegate; dispatch_queue_t delegateQueue; int socket4FD; int socket6FD; int socketUN; NSURL *socketUrl; int stateIndex; NSData * connectInterface4; NSData * connectInterface6; NSData * connectInterfaceUN; dispatch_queue_t socketQueue; dispatch_source_t accept4Source; dispatch_source_t accept6Source; dispatch_source_t acceptUNSource; dispatch_source_t connectTimer; dispatch_source_t readSource; dispatch_source_t writeSource; dispatch_source_t readTimer; dispatch_source_t writeTimer; NSMutableArray *readQueue; NSMutableArray *writeQueue; GCDAsyncReadPacket *currentRead; GCDAsyncWritePacket *currentWrite; unsigned long socketFDBytesAvailable; GCDAsyncSocketPreBuffer *preBuffer; #if TARGET_OS_IPHONE CFStreamClientContext streamContext; CFReadStreamRef readStream; CFWriteStreamRef writeStream; #endif SSLContextRef sslContext; GCDAsyncSocketPreBuffer *sslPreBuffer; size_t sslWriteCachedLength; OSStatus sslErrCode; OSStatus lastSSLHandshakeError; void *IsOnSocketQueueOrTargetQueueKey; id userData; NSTimeInterval alternateAddressDelay; } - (id)init { return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; } - (id)initWithSocketQueue:(dispatch_queue_t)sq { return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; } - (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq { return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; } - (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq { if((self = [super init])) { delegate = aDelegate; delegateQueue = dq; #if !OS_OBJECT_USE_OBJC if (dq) dispatch_retain(dq); #endif socket4FD = SOCKET_NULL; socket6FD = SOCKET_NULL; socketUN = SOCKET_NULL; socketUrl = nil; stateIndex = 0; if (sq) { NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), @"The given socketQueue parameter must not be a concurrent queue."); socketQueue = sq; #if !OS_OBJECT_USE_OBJC dispatch_retain(sq); #endif } else { socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); } // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. // From the documentation: // // > Keys are only compared as pointers and are never dereferenced. // > Thus, you can use a pointer to a static variable for a specific subsystem or // > any other value that allows you to identify the value uniquely. // // We're just going to use the memory address of an ivar. // Specifically an ivar that is explicitly named for our purpose to make the code more readable. // // However, it feels tedious (and less readable) to include the "&" all the time: // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) // // So we're going to make it so it doesn't matter if we use the '&' or not, // by assigning the value of the ivar to the address of the ivar. // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); readQueue = [[NSMutableArray alloc] initWithCapacity:5]; currentRead = nil; writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; currentWrite = nil; preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; alternateAddressDelay = 0.3; } return self; } - (void)dealloc { LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); // Set dealloc flag. // This is used by closeWithError to ensure we don't accidentally retain ourself. flags |= kDealloc; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { [self closeWithError:nil]; } else { dispatch_sync(socketQueue, ^{ [self closeWithError:nil]; }); } delegate = nil; #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_release(delegateQueue); #endif delegateQueue = NULL; #if !OS_OBJECT_USE_OBJC if (socketQueue) dispatch_release(socketQueue); #endif socketQueue = NULL; LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); } #pragma mark - + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD socketQueue:(nullable dispatch_queue_t)sq error:(NSError**)error { return [self socketFromConnectedSocketFD:socketFD delegate:nil delegateQueue:NULL socketQueue:sq error:error]; } + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq error:(NSError**)error { return [self socketFromConnectedSocketFD:socketFD delegate:aDelegate delegateQueue:dq socketQueue:NULL error:error]; } + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq error:(NSError* __autoreleasing *)error { __block BOOL errorOccured = NO; GCDAsyncSocket *socket = [[[self class] alloc] initWithDelegate:aDelegate delegateQueue:dq socketQueue:sq]; dispatch_sync(socket->socketQueue, ^{ @autoreleasepool { struct sockaddr addr; socklen_t addr_size = sizeof(struct sockaddr); int retVal = getpeername(socketFD, (struct sockaddr *)&addr, &addr_size); if (retVal) { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to create socket from socket FD failed. getpeername() failed", nil); NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; errorOccured = YES; if (error) *error = [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; return; } if (addr.sa_family == AF_INET) { socket->socket4FD = socketFD; } else if (addr.sa_family == AF_INET6) { socket->socket6FD = socketFD; } else { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to create socket from socket FD failed. socket FD is neither IPv4 nor IPv6", nil); NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; errorOccured = YES; if (error) *error = [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; return; } socket->flags = kSocketStarted; [socket didConnect:socket->stateIndex]; }}); return errorOccured? nil: socket; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Configuration //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (id)delegate { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return delegate; } else { __block id result; dispatch_sync(socketQueue, ^{ result = delegate; }); return result; } } - (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ delegate = newDelegate; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegate:(id)newDelegate { [self setDelegate:newDelegate synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate { [self setDelegate:newDelegate synchronously:YES]; } - (dispatch_queue_t)delegateQueue { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return delegateQueue; } else { __block dispatch_queue_t result; dispatch_sync(socketQueue, ^{ result = delegateQueue; }); return result; } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_release(delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif delegateQueue = newDelegateQueue; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegateQueue:newDelegateQueue synchronously:YES]; } - (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (delegatePtr) *delegatePtr = delegate; if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; } else { __block id dPtr = NULL; __block dispatch_queue_t dqPtr = NULL; dispatch_sync(socketQueue, ^{ dPtr = delegate; dqPtr = delegateQueue; }); if (delegatePtr) *delegatePtr = dPtr; if (delegateQueuePtr) *delegateQueuePtr = dqPtr; } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ delegate = newDelegate; #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_release(delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif delegateQueue = newDelegateQueue; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; } - (BOOL)isIPv4Enabled { // Note: YES means kIPv4Disabled is OFF if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kIPv4Disabled) == 0); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = ((config & kIPv4Disabled) == 0); }); return result; } } - (void)setIPv4Enabled:(BOOL)flag { // Note: YES means kIPv4Disabled is OFF dispatch_block_t block = ^{ if (flag) config &= ~kIPv4Disabled; else config |= kIPv4Disabled; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (BOOL)isIPv6Enabled { // Note: YES means kIPv6Disabled is OFF if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kIPv6Disabled) == 0); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = ((config & kIPv6Disabled) == 0); }); return result; } } - (void)setIPv6Enabled:(BOOL)flag { // Note: YES means kIPv6Disabled is OFF dispatch_block_t block = ^{ if (flag) config &= ~kIPv6Disabled; else config |= kIPv6Disabled; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (BOOL)isIPv4PreferredOverIPv6 { // Note: YES means kPreferIPv6 is OFF if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kPreferIPv6) == 0); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = ((config & kPreferIPv6) == 0); }); return result; } } - (void)setIPv4PreferredOverIPv6:(BOOL)flag { // Note: YES means kPreferIPv6 is OFF dispatch_block_t block = ^{ if (flag) config &= ~kPreferIPv6; else config |= kPreferIPv6; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (NSTimeInterval) alternateAddressDelay { __block NSTimeInterval delay; dispatch_block_t block = ^{ delay = alternateAddressDelay; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return delay; } - (void) setAlternateAddressDelay:(NSTimeInterval)delay { dispatch_block_t block = ^{ alternateAddressDelay = delay; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (id)userData { __block id result = nil; dispatch_block_t block = ^{ result = userData; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (void)setUserData:(id)arbitraryUserData { dispatch_block_t block = ^{ if (userData != arbitraryUserData) { userData = arbitraryUserData; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Accepting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr { return [self acceptOnInterface:nil port:port error:errPtr]; } - (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr { LogTrace(); // Just in-case interface parameter is immutable. NSString *interface = [inInterface copy]; __block BOOL result = NO; __block NSError *err = nil; // CreateSocket Block // This block will be invoked within the dispatch block below. int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { int socketFD = socket(domain, SOCK_STREAM, 0); if (socketFD == SOCKET_NULL) { NSString *reason = @"Error in socket() function"; err = [self errnoErrorWithReason:reason]; return SOCKET_NULL; } int status; // Set socket options status = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (status == -1) { NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; err = [self errnoErrorWithReason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } int reuseOn = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); if (status == -1) { NSString *reason = @"Error enabling address reuse (setsockopt)"; err = [self errnoErrorWithReason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } // Bind socket status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); if (status == -1) { NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } // Listen status = listen(socketFD, 1024); if (status == -1) { NSString *reason = @"Error in listen() function"; err = [self errnoErrorWithReason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } return socketFD; }; // Create dispatch block and run on socketQueue dispatch_block_t block = ^{ @autoreleasepool { if (delegate == nil) // Must have delegate set { NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; err = [self badConfigError:msg]; return_from_block; } if (delegateQueue == NULL) // Must have delegate queue set { NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; err = [self badConfigError:msg]; return_from_block; } BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; err = [self badConfigError:msg]; return_from_block; } if (![self isDisconnected]) // Must be disconnected { NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; err = [self badConfigError:msg]; return_from_block; } // Clear queues (spurious read/write requests post disconnect) [readQueue removeAllObjects]; [writeQueue removeAllObjects]; // Resolve interface from description NSMutableData *interface4 = nil; NSMutableData *interface6 = nil; [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; if ((interface4 == nil) && (interface6 == nil)) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; err = [self badParamError:msg]; return_from_block; } if (isIPv4Disabled && (interface6 == nil)) { NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; err = [self badParamError:msg]; return_from_block; } if (isIPv6Disabled && (interface4 == nil)) { NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; err = [self badParamError:msg]; return_from_block; } BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); // Create sockets, configure, bind, and listen if (enableIPv4) { LogVerbose(@"Creating IPv4 socket"); socket4FD = createSocket(AF_INET, interface4); if (socket4FD == SOCKET_NULL) { return_from_block; } } if (enableIPv6) { LogVerbose(@"Creating IPv6 socket"); if (enableIPv4 && (port == 0)) { // No specific port was specified, so we allowed the OS to pick an available port for us. // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; addr6->sin6_port = htons([self localPort4]); } socket6FD = createSocket(AF_INET6, interface6); if (socket6FD == SOCKET_NULL) { if (socket4FD != SOCKET_NULL) { LogVerbose(@"close(socket4FD)"); close(socket4FD); } return_from_block; } } // Create accept sources if (enableIPv4) { accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); int socketFD = socket4FD; dispatch_source_t acceptSource = accept4Source; __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; LogVerbose(@"event4Block"); unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); LogVerbose(@"numPendingConnections: %lu", numPendingConnections); while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); #pragma clang diagnostic pop }}); dispatch_source_set_cancel_handler(accept4Source, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(accept4Source)"); dispatch_release(acceptSource); #endif LogVerbose(@"close(socket4FD)"); close(socketFD); #pragma clang diagnostic pop }); LogVerbose(@"dispatch_resume(accept4Source)"); dispatch_resume(accept4Source); } if (enableIPv6) { accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); int socketFD = socket6FD; dispatch_source_t acceptSource = accept6Source; __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(accept6Source, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; LogVerbose(@"event6Block"); unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); LogVerbose(@"numPendingConnections: %lu", numPendingConnections); while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); #pragma clang diagnostic pop }}); dispatch_source_set_cancel_handler(accept6Source, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(accept6Source)"); dispatch_release(acceptSource); #endif LogVerbose(@"close(socket6FD)"); close(socketFD); #pragma clang diagnostic pop }); LogVerbose(@"dispatch_resume(accept6Source)"); dispatch_resume(accept6Source); } flags |= kSocketStarted; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (result == NO) { LogInfo(@"Error in accept: %@", err); if (errPtr) *errPtr = err; } return result; } - (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr { LogTrace(); __block BOOL result = NO; __block NSError *err = nil; // CreateSocket Block // This block will be invoked within the dispatch block below. int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { int socketFD = socket(domain, SOCK_STREAM, 0); if (socketFD == SOCKET_NULL) { NSString *reason = @"Error in socket() function"; err = [self errnoErrorWithReason:reason]; return SOCKET_NULL; } int status; // Set socket options status = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (status == -1) { NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; err = [self errnoErrorWithReason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } int reuseOn = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); if (status == -1) { NSString *reason = @"Error enabling address reuse (setsockopt)"; err = [self errnoErrorWithReason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } // Bind socket status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); if (status == -1) { NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } // Listen status = listen(socketFD, 1024); if (status == -1) { NSString *reason = @"Error in listen() function"; err = [self errnoErrorWithReason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } return socketFD; }; // Create dispatch block and run on socketQueue dispatch_block_t block = ^{ @autoreleasepool { if (delegate == nil) // Must have delegate set { NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; err = [self badConfigError:msg]; return_from_block; } if (delegateQueue == NULL) // Must have delegate queue set { NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; err = [self badConfigError:msg]; return_from_block; } if (![self isDisconnected]) // Must be disconnected { NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; err = [self badConfigError:msg]; return_from_block; } // Clear queues (spurious read/write requests post disconnect) [readQueue removeAllObjects]; [writeQueue removeAllObjects]; // Remove a previous socket NSError *error = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:url.path]) { if (![[NSFileManager defaultManager] removeItemAtURL:url error:&error]) { NSString *msg = @"Could not remove previous unix domain socket at given url."; err = [self otherError:msg]; return_from_block; } } // Resolve interface from description NSData *interface = [self getInterfaceAddressFromUrl:url]; if (interface == nil) { NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; err = [self badParamError:msg]; return_from_block; } // Create sockets, configure, bind, and listen LogVerbose(@"Creating unix domain socket"); socketUN = createSocket(AF_UNIX, interface); if (socketUN == SOCKET_NULL) { return_from_block; } socketUrl = url; // Create accept sources acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketUN, 0, socketQueue); int socketFD = socketUN; dispatch_source_t acceptSource = acceptUNSource; dispatch_source_set_event_handler(acceptUNSource, ^{ @autoreleasepool { LogVerbose(@"eventUNBlock"); unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); LogVerbose(@"numPendingConnections: %lu", numPendingConnections); while ([self doAccept:socketFD] && (++i < numPendingConnections)); }}); dispatch_source_set_cancel_handler(acceptUNSource, ^{ #if NEEDS_DISPATCH_RETAIN_RELEASE LogVerbose(@"dispatch_release(accept4Source)"); dispatch_release(acceptSource); #endif LogVerbose(@"close(socket4FD)"); close(socketFD); }); LogVerbose(@"dispatch_resume(accept4Source)"); dispatch_resume(acceptUNSource); flags |= kSocketStarted; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (result == NO) { LogInfo(@"Error in accept: %@", err); if (errPtr) *errPtr = err; } return result; } - (BOOL)doAccept:(int)parentSocketFD { LogTrace(); int socketType; int childSocketFD; NSData *childSocketAddress; if (parentSocketFD == socket4FD) { socketType = 0; struct sockaddr_in addr; socklen_t addrLen = sizeof(addr); childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } else if (parentSocketFD == socket6FD) { socketType = 1; struct sockaddr_in6 addr; socklen_t addrLen = sizeof(addr); childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } else // if (parentSocketFD == socketUN) { socketType = 2; struct sockaddr_un addr; socklen_t addrLen = sizeof(addr); childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } // Enable non-blocking IO on the socket int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); if (result == -1) { LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); LogVerbose(@"close(childSocketFD)"); close(childSocketFD); return NO; } // Prevent SIGPIPE signals int nosigpipe = 1; setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); // Notify delegate if (delegateQueue) { __strong id theDelegate = delegate; dispatch_async(delegateQueue, ^{ @autoreleasepool { // Query delegate for custom socket queue dispatch_queue_t childSocketQueue = NULL; if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) { childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress onSocket:self]; } // Create GCDAsyncSocket instance for accepted socket GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate delegateQueue:delegateQueue socketQueue:childSocketQueue]; if (socketType == 0) acceptedSocket->socket4FD = childSocketFD; else if (socketType == 1) acceptedSocket->socket6FD = childSocketFD; else acceptedSocket->socketUN = childSocketFD; acceptedSocket->flags = (kSocketStarted | kConnected); // Setup read and write sources for accepted socket dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; }}); // Notify delegate if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) { [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; } // Release the socket queue returned from the delegate (it was retained by acceptedSocket) #if !OS_OBJECT_USE_OBJC if (childSocketQueue) dispatch_release(childSocketQueue); #endif // The accepted socket should have been retained by the delegate. // Otherwise it gets properly released when exiting the block. }}); } return YES; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Connecting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * This method runs through the various checks required prior to a connection attempt. * It is shared between the connectToHost and connectToAddress methods. * **/ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (delegate == nil) // Must have delegate set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; *errPtr = [self badConfigError:msg]; } return NO; } if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; *errPtr = [self badConfigError:msg]; } return NO; } if (![self isDisconnected]) // Must be disconnected { if (errPtr) { NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; *errPtr = [self badConfigError:msg]; } return NO; } BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { if (errPtr) { NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; *errPtr = [self badConfigError:msg]; } return NO; } if (interface) { NSMutableData *interface4 = nil; NSMutableData *interface6 = nil; [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; if ((interface4 == nil) && (interface6 == nil)) { if (errPtr) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; *errPtr = [self badParamError:msg]; } return NO; } if (isIPv4Disabled && (interface6 == nil)) { if (errPtr) { NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; *errPtr = [self badParamError:msg]; } return NO; } if (isIPv6Disabled && (interface4 == nil)) { if (errPtr) { NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; *errPtr = [self badParamError:msg]; } return NO; } connectInterface4 = interface4; connectInterface6 = interface6; } // Clear queues (spurious read/write requests post disconnect) [readQueue removeAllObjects]; [writeQueue removeAllObjects]; return YES; } - (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (delegate == nil) // Must have delegate set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; *errPtr = [self badConfigError:msg]; } return NO; } if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; *errPtr = [self badConfigError:msg]; } return NO; } if (![self isDisconnected]) // Must be disconnected { if (errPtr) { NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; *errPtr = [self badConfigError:msg]; } return NO; } NSData *interface = [self getInterfaceAddressFromUrl:url]; if (interface == nil) { if (errPtr) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; *errPtr = [self badParamError:msg]; } return NO; } connectInterfaceUN = interface; // Clear queues (spurious read/write requests post disconnect) [readQueue removeAllObjects]; [writeQueue removeAllObjects]; return YES; } - (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr { return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; } - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; } - (BOOL)connectToHost:(NSString *)inHost onPort:(uint16_t)port viaInterface:(NSString *)inInterface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { LogTrace(); // Just in case immutable objects were passed NSString *host = [inHost copy]; NSString *interface = [inInterface copy]; __block BOOL result = NO; __block NSError *preConnectErr = nil; dispatch_block_t block = ^{ @autoreleasepool { // Check for problems with host parameter if ([host length] == 0) { NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; preConnectErr = [self badParamError:msg]; return_from_block; } // Run through standard pre-connect checks if (![self preConnectWithInterface:interface error:&preConnectErr]) { return_from_block; } // We've made it past all the checks. // It's time to start the connection process. flags |= kSocketStarted; LogVerbose(@"Dispatching DNS lookup..."); // It's possible that the given host parameter is actually a NSMutableString. // So we want to copy it now, within this block that will be executed synchronously. // This way the asynchronous lookup block below doesn't have to worry about it changing. NSString *hostCpy = [host copy]; int aStateIndex = stateIndex; __weak GCDAsyncSocket *weakSelf = self; dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" NSError *lookupErr = nil; NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; if (lookupErr) { dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didFail:lookupErr]; }}); } else { NSData *address4 = nil; NSData *address6 = nil; for (NSData *address in addresses) { if (!address4 && [[self class] isIPv4Address:address]) { address4 = address; } else if (!address6 && [[self class] isIPv6Address:address]) { address6 = address; } } dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; }}); } #pragma clang diagnostic pop }}); [self startConnectTimeout:timeout]; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (errPtr) *errPtr = preConnectErr; return result; } - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr { return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; } - (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; } - (BOOL)connectToAddress:(NSData *)inRemoteAddr viaInterface:(NSString *)inInterface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { LogTrace(); // Just in case immutable objects were passed NSData *remoteAddr = [inRemoteAddr copy]; NSString *interface = [inInterface copy]; __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Check for problems with remoteAddr parameter NSData *address4 = nil; NSData *address6 = nil; if ([remoteAddr length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; if (sockaddr->sa_family == AF_INET) { if ([remoteAddr length] == sizeof(struct sockaddr_in)) { address4 = remoteAddr; } } else if (sockaddr->sa_family == AF_INET6) { if ([remoteAddr length] == sizeof(struct sockaddr_in6)) { address6 = remoteAddr; } } } if ((address4 == nil) && (address6 == nil)) { NSString *msg = @"A valid IPv4 or IPv6 address was not given"; err = [self badParamError:msg]; return_from_block; } BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && (address4 != nil)) { NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; err = [self badParamError:msg]; return_from_block; } if (isIPv6Disabled && (address6 != nil)) { NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; err = [self badParamError:msg]; return_from_block; } // Run through standard pre-connect checks if (![self preConnectWithInterface:interface error:&err]) { return_from_block; } // We've made it past all the checks. // It's time to start the connection process. if (![self connectWithAddress4:address4 address6:address6 error:&err]) { return_from_block; } flags |= kSocketStarted; [self startConnectTimeout:timeout]; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (result == NO) { if (errPtr) *errPtr = err; } return result; } - (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { LogTrace(); __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Check for problems with host parameter if ([url.path length] == 0) { NSString *msg = @"Invalid unix domain socket url."; err = [self badParamError:msg]; return_from_block; } // Run through standard pre-connect checks if (![self preConnectWithUrl:url error:&err]) { return_from_block; } // We've made it past all the checks. // It's time to start the connection process. flags |= kSocketStarted; // Start the normal connection process NSError *connectError = nil; if (![self connectWithAddressUN:connectInterfaceUN error:&connectError]) { [self closeWithError:connectError]; return_from_block; } [self startConnectTimeout:timeout]; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (result == NO) { if (errPtr) *errPtr = err; } return result; } - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(address4 || address6, @"Expected at least one valid address"); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } // Check for problems BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && (address6 == nil)) { NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; [self closeWithError:[self otherError:msg]]; return; } if (isIPv6Disabled && (address4 == nil)) { NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; [self closeWithError:[self otherError:msg]]; return; } // Start the normal connection process NSError *err = nil; if (![self connectWithAddress4:address4 address6:address6 error:&err]) { [self closeWithError:err]; } } /** * This method is called if the DNS lookup fails. * This method is executed on the socketQueue. * * Since the DNS lookup executed synchronously on a global concurrent queue, * the original connection request may have already been cancelled or timed-out by the time this method is invoked. * The lookupIndex tells us whether the lookup is still valid or not. **/ - (void)lookup:(int)aStateIndex didFail:(NSError *)error { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring lookup:didFail: - already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } [self endConnectTimeout]; [self closeWithError:error]; } - (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr { // Bind the socket to the desired interface (if needed) if (connectInterface) { LogVerbose(@"Binding socket..."); if ([[self class] portFromAddress:connectInterface] > 0) { // Since we're going to be binding to a specific port, // we should turn on reuseaddr to allow us to override sockets in time_wait. int reuseOn = 1; setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); } const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); if (result != 0) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; return NO; } } return YES; } - (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr { int socketFD = socket(family, SOCK_STREAM, 0); if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; return socketFD; } if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) { [self closeSocket:socketFD]; return SOCKET_NULL; } // Prevent SIGPIPE signals int nosigpipe = 1; setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); return socketFD; } - (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex { // If there already is a socket connected, we close socketFD and return if (self.isConnected) { [self closeSocket:socketFD]; return; } // Start the connection process in a background queue __weak GCDAsyncSocket *weakSelf = self; dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { if (strongSelf.isConnected) { [strongSelf closeSocket:socketFD]; return_from_block; } if (result == 0) { [self closeUnusedSocket:socketFD]; [strongSelf didConnect:aStateIndex]; } else { [strongSelf closeSocket:socketFD]; // If there are no more sockets trying to connect, we inform the error to the delegate if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL) { NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"]; [strongSelf didNotConnect:aStateIndex error:error]; } } }}); #pragma clang diagnostic pop }); LogVerbose(@"Connecting..."); } - (void)closeSocket:(int)socketFD { if (socketFD != SOCKET_NULL && (socketFD == socket6FD || socketFD == socket4FD)) { close(socketFD); if (socketFD == socket4FD) { LogVerbose(@"close(socket4FD)"); socket4FD = SOCKET_NULL; } else if (socketFD == socket6FD) { LogVerbose(@"close(socket6FD)"); socket6FD = SOCKET_NULL; } } } - (void)closeUnusedSocket:(int)usedSocketFD { if (usedSocketFD != socket4FD) { [self closeSocket:socket4FD]; } else if (usedSocketFD != socket6FD) { [self closeSocket:socket6FD]; } } - (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); // Determine socket type BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; // Create and bind the sockets if (address4) { LogVerbose(@"Creating IPv4 socket"); socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; } if (address6) { LogVerbose(@"Creating IPv6 socket"); socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; } if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) { return NO; } int socketFD, alternateSocketFD; NSData *address, *alternateAddress; if ((preferIPv6 && socket6FD != SOCKET_NULL) || socket4FD == SOCKET_NULL) { socketFD = socket6FD; alternateSocketFD = socket4FD; address = address6; alternateAddress = address4; } else { socketFD = socket4FD; alternateSocketFD = socket6FD; address = address4; alternateAddress = address6; } int aStateIndex = stateIndex; [self connectSocket:socketFD address:address stateIndex:aStateIndex]; if (alternateAddress) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; }); } return YES; } - (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); // Create the socket int socketFD; LogVerbose(@"Creating unix domain socket"); socketUN = socket(AF_UNIX, SOCK_STREAM, 0); socketFD = socketUN; if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; return NO; } // Bind the socket to the desired interface (if needed) LogVerbose(@"Binding socket..."); int reuseOn = 1; setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); // const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; // // int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); // if (result != 0) // { // if (errPtr) // *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; // // return NO; // } // Prevent SIGPIPE signals int nosigpipe = 1; setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); // Start the connection process in a background queue int aStateIndex = stateIndex; dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; int result = connect(socketFD, addr, addr->sa_len); if (result == 0) { dispatch_async(socketQueue, ^{ @autoreleasepool { [self didConnect:aStateIndex]; }}); } else { // TODO: Bad file descriptor perror("connect"); NSError *error = [self errnoErrorWithReason:@"Error in connect() function"]; dispatch_async(socketQueue, ^{ @autoreleasepool { [self didNotConnect:aStateIndex error:error]; }}); } }); LogVerbose(@"Connecting..."); return YES; } - (void)didConnect:(int)aStateIndex { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring didConnect, already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } flags |= kConnected; [self endConnectTimeout]; #if TARGET_OS_IPHONE // The endConnectTimeout method executed above incremented the stateIndex. aStateIndex = stateIndex; #endif // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) // // Note: // There may be configuration options that must be set by the delegate before opening the streams. // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. // // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. // This gives the delegate time to properly configure the streams if needed. dispatch_block_t SetupStreamsPart1 = ^{ #if TARGET_OS_IPHONE if (![self createReadAndWriteStream]) { [self closeWithError:[self otherError:@"Error creating CFStreams"]]; return; } if (![self registerForStreamCallbacksIncludingReadWrite:NO]) { [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; return; } #endif }; dispatch_block_t SetupStreamsPart2 = ^{ #if TARGET_OS_IPHONE if (aStateIndex != stateIndex) { // The socket has been disconnected. return; } if (![self addStreamsToRunLoop]) { [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; return; } if (![self openStreams]) { [self closeWithError:[self otherError:@"Error creating CFStreams"]]; return; } #endif }; // Notify delegate NSString *host = [self connectedHost]; uint16_t port = [self connectedPort]; NSURL *url = [self connectedUrl]; __strong id theDelegate = delegate; if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) { SetupStreamsPart1(); dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didConnectToHost:host port:port]; dispatch_async(socketQueue, ^{ @autoreleasepool { SetupStreamsPart2(); }}); }}); } else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) { SetupStreamsPart1(); dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didConnectToUrl:url]; dispatch_async(socketQueue, ^{ @autoreleasepool { SetupStreamsPart2(); }}); }}); } else { SetupStreamsPart1(); SetupStreamsPart2(); } // Get the connected socket int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; // Enable non-blocking IO on the socket int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (result == -1) { NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; [self closeWithError:[self otherError:errMsg]]; return; } // Setup our read/write sources [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; // Dequeue any pending read/write requests [self maybeDequeueRead]; [self maybeDequeueWrite]; } - (void)didNotConnect:(int)aStateIndex error:(NSError *)error { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring didNotConnect, already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } [self closeWithError:error]; } - (void)startConnectTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { unsigned long connectTimerMask = 0; if (config & kUseStrictTimers) { connectTimerMask = DISPATCH_TIMER_STRICT; } connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, connectTimerMask, socketQueue); __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; [strongSelf doConnectTimeout]; #pragma clang diagnostic pop }}); #if !OS_OBJECT_USE_OBJC dispatch_source_t theConnectTimer = connectTimer; dispatch_source_set_cancel_handler(connectTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"dispatch_release(connectTimer)"); dispatch_release(theConnectTimer); #pragma clang diagnostic pop }); #endif dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(connectTimer); } } - (void)endConnectTimeout { LogTrace(); if (connectTimer) { dispatch_source_cancel(connectTimer); connectTimer = NULL; } // Increment stateIndex. // This will prevent us from processing results from any related background asynchronous operations. // // Note: This should be called from close method even if connectTimer is NULL. // This is because one might disconnect a socket prior to a successful connection which had no timeout. stateIndex++; if (connectInterface4) { connectInterface4 = nil; } if (connectInterface6) { connectInterface6 = nil; } } - (void)doConnectTimeout { LogTrace(); [self endConnectTimeout]; [self closeWithError:[self connectTimeoutError]]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Disconnecting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)closeWithError:(NSError *)error { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); [self endConnectTimeout]; if (currentRead != nil) [self endCurrentRead]; if (currentWrite != nil) [self endCurrentWrite]; [readQueue removeAllObjects]; [writeQueue removeAllObjects]; [preBuffer reset]; #if TARGET_OS_IPHONE { if (readStream || writeStream) { [self removeStreamsFromRunLoop]; if (readStream) { CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); CFReadStreamClose(readStream); CFRelease(readStream); readStream = NULL; } if (writeStream) { CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); CFWriteStreamClose(writeStream); CFRelease(writeStream); writeStream = NULL; } } } #endif [sslPreBuffer reset]; sslErrCode = lastSSLHandshakeError = noErr; if (sslContext) { // Getting a linker error here about the SSLx() functions? // You need to add the Security Framework to your application. SSLClose(sslContext); #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) CFRelease(sslContext); #else SSLDisposeContext(sslContext); #endif sslContext = NULL; } // For some crazy reason (in my opinion), cancelling a dispatch source doesn't // invoke the cancel handler if the dispatch source is paused. // So we have to unpause the source if needed. // This allows the cancel handler to be run, which in turn releases the source and closes the socket. if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) { LogVerbose(@"manually closing close"); if (socket4FD != SOCKET_NULL) { LogVerbose(@"close(socket4FD)"); close(socket4FD); socket4FD = SOCKET_NULL; } if (socket6FD != SOCKET_NULL) { LogVerbose(@"close(socket6FD)"); close(socket6FD); socket6FD = SOCKET_NULL; } if (socketUN != SOCKET_NULL) { LogVerbose(@"close(socketUN)"); close(socketUN); socketUN = SOCKET_NULL; unlink(socketUrl.path.fileSystemRepresentation); socketUrl = nil; } } else { if (accept4Source) { LogVerbose(@"dispatch_source_cancel(accept4Source)"); dispatch_source_cancel(accept4Source); // We never suspend accept4Source accept4Source = NULL; } if (accept6Source) { LogVerbose(@"dispatch_source_cancel(accept6Source)"); dispatch_source_cancel(accept6Source); // We never suspend accept6Source accept6Source = NULL; } if (acceptUNSource) { LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); dispatch_source_cancel(acceptUNSource); // We never suspend acceptUNSource acceptUNSource = NULL; } if (readSource) { LogVerbose(@"dispatch_source_cancel(readSource)"); dispatch_source_cancel(readSource); [self resumeReadSource]; readSource = NULL; } if (writeSource) { LogVerbose(@"dispatch_source_cancel(writeSource)"); dispatch_source_cancel(writeSource); [self resumeWriteSource]; writeSource = NULL; } // The sockets will be closed by the cancel handlers of the corresponding source socket4FD = SOCKET_NULL; socket6FD = SOCKET_NULL; socketUN = SOCKET_NULL; } // If the client has passed the connect/accept method, then the connection has at least begun. // Notify delegate that it is now ending. BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; BOOL isDeallocating = (flags & kDealloc) ? YES : NO; // Clear stored socket info and all flags (config remains as is) socketFDBytesAvailable = 0; flags = 0; sslWriteCachedLength = 0; if (shouldCallDelegate) { __strong id theDelegate = delegate; __strong id theSelf = isDeallocating ? nil : self; if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidDisconnect:theSelf withError:error]; }}); } } } - (void)disconnect { dispatch_block_t block = ^{ @autoreleasepool { if (flags & kSocketStarted) { [self closeWithError:nil]; } }}; // Synchronous disconnection, as documented in the header file if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); } - (void)disconnectAfterReading { dispatch_async(socketQueue, ^{ @autoreleasepool { if (flags & kSocketStarted) { flags |= (kForbidReadsWrites | kDisconnectAfterReads); [self maybeClose]; } }}); } - (void)disconnectAfterWriting { dispatch_async(socketQueue, ^{ @autoreleasepool { if (flags & kSocketStarted) { flags |= (kForbidReadsWrites | kDisconnectAfterWrites); [self maybeClose]; } }}); } - (void)disconnectAfterReadingAndWriting { dispatch_async(socketQueue, ^{ @autoreleasepool { if (flags & kSocketStarted) { flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); [self maybeClose]; } }}); } /** * Closes the socket if possible. * That is, if all writes have completed, and we're set to disconnect after writing, * or if all reads have completed, and we're set to disconnect after reading. **/ - (void)maybeClose { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); BOOL shouldClose = NO; if (flags & kDisconnectAfterReads) { if (([readQueue count] == 0) && (currentRead == nil)) { if (flags & kDisconnectAfterWrites) { if (([writeQueue count] == 0) && (currentWrite == nil)) { shouldClose = YES; } } else { shouldClose = YES; } } } else if (flags & kDisconnectAfterWrites) { if (([writeQueue count] == 0) && (currentWrite == nil)) { shouldClose = YES; } } if (shouldClose) { [self closeWithError:nil]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Errors //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSError *)badConfigError:(NSString *)errMsg { NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; } - (NSError *)badParamError:(NSString *)errMsg { NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; } + (NSError *)gaiError:(int)gai_error { NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; } - (NSError *)errnoErrorWithReason:(NSString *)reason { NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, reason, NSLocalizedFailureReasonErrorKey, nil]; return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; } - (NSError *)errnoError { NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; } - (NSError *)sslError:(OSStatus)ssl_error { NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey]; return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; } - (NSError *)connectTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to connect to host timed out", nil); NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; } /** * Returns a standard AsyncSocket maxed out error. **/ - (NSError *)readMaxedOutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Read operation reached set maximum length", nil); NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; } /** * Returns a standard AsyncSocket write timeout error. **/ - (NSError *)readTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Read operation timed out", nil); NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; } /** * Returns a standard AsyncSocket write timeout error. **/ - (NSError *)writeTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Write operation timed out", nil); NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; } - (NSError *)connectionClosedError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Socket closed by remote peer", nil); NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; } - (NSError *)otherError:(NSString *)errMsg { NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Diagnostics //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)isDisconnected { __block BOOL result = NO; dispatch_block_t block = ^{ result = (flags & kSocketStarted) ? NO : YES; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isConnected { __block BOOL result = NO; dispatch_block_t block = ^{ result = (flags & kConnected) ? YES : NO; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (NSString *)connectedHost { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socket4FD != SOCKET_NULL) return [self connectedHostFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self connectedHostFromSocket6:socket6FD]; return nil; } else { __block NSString *result = nil; dispatch_sync(socketQueue, ^{ @autoreleasepool { if (socket4FD != SOCKET_NULL) result = [self connectedHostFromSocket4:socket4FD]; else if (socket6FD != SOCKET_NULL) result = [self connectedHostFromSocket6:socket6FD]; }}); return result; } } - (uint16_t)connectedPort { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socket4FD != SOCKET_NULL) return [self connectedPortFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self connectedPortFromSocket6:socket6FD]; return 0; } else { __block uint16_t result = 0; dispatch_sync(socketQueue, ^{ // No need for autorelease pool if (socket4FD != SOCKET_NULL) result = [self connectedPortFromSocket4:socket4FD]; else if (socket6FD != SOCKET_NULL) result = [self connectedPortFromSocket6:socket6FD]; }); return result; } } - (NSURL *)connectedUrl { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socketUN != SOCKET_NULL) return [self connectedUrlFromSocketUN:socketUN]; return nil; } else { __block NSURL *result = nil; dispatch_sync(socketQueue, ^{ @autoreleasepool { if (socketUN != SOCKET_NULL) result = [self connectedUrlFromSocketUN:socketUN]; }}); return result; } } - (NSString *)localHost { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socket4FD != SOCKET_NULL) return [self localHostFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self localHostFromSocket6:socket6FD]; return nil; } else { __block NSString *result = nil; dispatch_sync(socketQueue, ^{ @autoreleasepool { if (socket4FD != SOCKET_NULL) result = [self localHostFromSocket4:socket4FD]; else if (socket6FD != SOCKET_NULL) result = [self localHostFromSocket6:socket6FD]; }}); return result; } } - (uint16_t)localPort { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socket4FD != SOCKET_NULL) return [self localPortFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self localPortFromSocket6:socket6FD]; return 0; } else { __block uint16_t result = 0; dispatch_sync(socketQueue, ^{ // No need for autorelease pool if (socket4FD != SOCKET_NULL) result = [self localPortFromSocket4:socket4FD]; else if (socket6FD != SOCKET_NULL) result = [self localPortFromSocket6:socket6FD]; }); return result; } } - (NSString *)connectedHost4 { if (socket4FD != SOCKET_NULL) return [self connectedHostFromSocket4:socket4FD]; return nil; } - (NSString *)connectedHost6 { if (socket6FD != SOCKET_NULL) return [self connectedHostFromSocket6:socket6FD]; return nil; } - (uint16_t)connectedPort4 { if (socket4FD != SOCKET_NULL) return [self connectedPortFromSocket4:socket4FD]; return 0; } - (uint16_t)connectedPort6 { if (socket6FD != SOCKET_NULL) return [self connectedPortFromSocket6:socket6FD]; return 0; } - (NSString *)localHost4 { if (socket4FD != SOCKET_NULL) return [self localHostFromSocket4:socket4FD]; return nil; } - (NSString *)localHost6 { if (socket6FD != SOCKET_NULL) return [self localHostFromSocket6:socket6FD]; return nil; } - (uint16_t)localPort4 { if (socket4FD != SOCKET_NULL) return [self localPortFromSocket4:socket4FD]; return 0; } - (uint16_t)localPort6 { if (socket6FD != SOCKET_NULL) return [self localPortFromSocket6:socket6FD]; return 0; } - (NSString *)connectedHostFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return nil; } return [[self class] hostFromSockaddr4:&sockaddr4]; } - (NSString *)connectedHostFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return nil; } return [[self class] hostFromSockaddr6:&sockaddr6]; } - (uint16_t)connectedPortFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return 0; } return [[self class] portFromSockaddr4:&sockaddr4]; } - (uint16_t)connectedPortFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return 0; } return [[self class] portFromSockaddr6:&sockaddr6]; } - (NSURL *)connectedUrlFromSocketUN:(int)socketFD { struct sockaddr_un sockaddr; socklen_t sockaddrlen = sizeof(sockaddr); if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) { return 0; } return [[self class] urlFromSockaddrUN:&sockaddr]; } - (NSString *)localHostFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return nil; } return [[self class] hostFromSockaddr4:&sockaddr4]; } - (NSString *)localHostFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return nil; } return [[self class] hostFromSockaddr6:&sockaddr6]; } - (uint16_t)localPortFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return 0; } return [[self class] portFromSockaddr4:&sockaddr4]; } - (uint16_t)localPortFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return 0; } return [[self class] portFromSockaddr6:&sockaddr6]; } - (NSData *)connectedAddress { __block NSData *result = nil; dispatch_block_t block = ^{ if (socket4FD != SOCKET_NULL) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; } } if (socket6FD != SOCKET_NULL) { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; } } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (NSData *)localAddress { __block NSData *result = nil; dispatch_block_t block = ^{ if (socket4FD != SOCKET_NULL) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; } } if (socket6FD != SOCKET_NULL) { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; } } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isIPv4 { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return (socket4FD != SOCKET_NULL); } else { __block BOOL result = NO; dispatch_sync(socketQueue, ^{ result = (socket4FD != SOCKET_NULL); }); return result; } } - (BOOL)isIPv6 { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return (socket6FD != SOCKET_NULL); } else { __block BOOL result = NO; dispatch_sync(socketQueue, ^{ result = (socket6FD != SOCKET_NULL); }); return result; } } - (BOOL)isSecure { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return (flags & kSocketSecure) ? YES : NO; } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = (flags & kSocketSecure) ? YES : NO; }); return result; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Finds the address of an interface description. * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). * * The interface description may optionally contain a port number at the end, separated by a colon. * If a non-zero port parameter is provided, any port number in the interface description is ignored. * * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. **/ - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr address6:(NSMutableData **)interfaceAddr6Ptr fromDescription:(NSString *)interfaceDescription port:(uint16_t)port { NSMutableData *addr4 = nil; NSMutableData *addr6 = nil; NSString *interface = nil; NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; if ([components count] > 0) { NSString *temp = [components objectAtIndex:0]; if ([temp length] > 0) { interface = temp; } } if ([components count] > 1 && port == 0) { long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); if (portL > 0 && portL <= UINT16_MAX) { port = (uint16_t)portL; } } if (interface == nil) { // ANY address struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); sockaddr4.sin_len = sizeof(sockaddr4); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_any; addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) { // LOOPBACK address struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); sockaddr4.sin_len = sizeof(sockaddr4); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_loopback; addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else { const char *iface = [interface UTF8String]; struct ifaddrs *addrs; const struct ifaddrs *cursor; if ((getifaddrs(&addrs) == 0)) { cursor = addrs; while (cursor != NULL) { if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) { // IPv4 struct sockaddr_in nativeAddr4; memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); if (strcmp(cursor->ifa_name, iface) == 0) { // Name match nativeAddr4.sin_port = htons(port); addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } else { char ip[INET_ADDRSTRLEN]; const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match nativeAddr4.sin_port = htons(port); addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } } } else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) { // IPv6 struct sockaddr_in6 nativeAddr6; memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); if (strcmp(cursor->ifa_name, iface) == 0) { // Name match nativeAddr6.sin6_port = htons(port); addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } else { char ip[INET6_ADDRSTRLEN]; const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match nativeAddr6.sin6_port = htons(port); addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } } } cursor = cursor->ifa_next; } freeifaddrs(addrs); } } if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; } - (NSData *)getInterfaceAddressFromUrl:(NSURL *)url { NSString *path = url.path; if (path.length == 0) { return nil; } struct sockaddr_un nativeAddr; nativeAddr.sun_family = AF_UNIX; strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path)); nativeAddr.sun_len = SUN_LEN(&nativeAddr); NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; return interface; } - (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD { readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); // Setup event handlers __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; LogVerbose(@"readEventBlock"); strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); if (strongSelf->socketFDBytesAvailable > 0) [strongSelf doReadData]; else [strongSelf doReadEOF]; #pragma clang diagnostic pop }}); dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; LogVerbose(@"writeEventBlock"); strongSelf->flags |= kSocketCanAcceptBytes; [strongSelf doWriteData]; #pragma clang diagnostic pop }}); // Setup cancel handlers __block int socketFDRefCount = 2; #if !OS_OBJECT_USE_OBJC dispatch_source_t theReadSource = readSource; dispatch_source_t theWriteSource = writeSource; #endif dispatch_source_set_cancel_handler(readSource, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"readCancelBlock"); #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(readSource)"); dispatch_release(theReadSource); #endif if (--socketFDRefCount == 0) { LogVerbose(@"close(socketFD)"); close(socketFD); } #pragma clang diagnostic pop }); dispatch_source_set_cancel_handler(writeSource, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"writeCancelBlock"); #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(writeSource)"); dispatch_release(theWriteSource); #endif if (--socketFDRefCount == 0) { LogVerbose(@"close(socketFD)"); close(socketFD); } #pragma clang diagnostic pop }); // We will not be able to read until data arrives. // But we should be able to write immediately. socketFDBytesAvailable = 0; flags &= ~kReadSourceSuspended; LogVerbose(@"dispatch_resume(readSource)"); dispatch_resume(readSource); flags |= kSocketCanAcceptBytes; flags |= kWriteSourceSuspended; } - (BOOL)usingCFStreamForTLS { #if TARGET_OS_IPHONE if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) { // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. return YES; } #endif return NO; } - (BOOL)usingSecureTransportForTLS { // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) #if TARGET_OS_IPHONE if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) { // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. return NO; } #endif return YES; } - (void)suspendReadSource { if (!(flags & kReadSourceSuspended)) { LogVerbose(@"dispatch_suspend(readSource)"); dispatch_suspend(readSource); flags |= kReadSourceSuspended; } } - (void)resumeReadSource { if (flags & kReadSourceSuspended) { LogVerbose(@"dispatch_resume(readSource)"); dispatch_resume(readSource); flags &= ~kReadSourceSuspended; } } - (void)suspendWriteSource { if (!(flags & kWriteSourceSuspended)) { LogVerbose(@"dispatch_suspend(writeSource)"); dispatch_suspend(writeSource); flags |= kWriteSourceSuspended; } } - (void)resumeWriteSource { if (flags & kWriteSourceSuspended) { LogVerbose(@"dispatch_resume(writeSource)"); dispatch_resume(writeSource); flags &= ~kWriteSourceSuspended; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Reading //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag { [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; } - (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag { [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; } - (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)length tag:(long)tag { if (offset > [buffer length]) { LogWarn(@"Cannot read: offset > [buffer length]"); return; } GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:length timeout:timeout readLength:0 terminator:nil tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { LogTrace(); if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) { [readQueue addObject:packet]; [self maybeDequeueRead]; } }}); // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag { [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; } - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag { if (length == 0) { LogWarn(@"Cannot read: length == 0"); return; } if (offset > [buffer length]) { LogWarn(@"Cannot read: offset > [buffer length]"); return; } GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:0 timeout:timeout readLength:length terminator:nil tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { LogTrace(); if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) { [readQueue addObject:packet]; [self maybeDequeueRead]; } }}); // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag { [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag { [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)maxLength tag:(long)tag { if ([data length] == 0) { LogWarn(@"Cannot read: [data length] == 0"); return; } if (offset > [buffer length]) { LogWarn(@"Cannot read: offset > [buffer length]"); return; } if (maxLength > 0 && maxLength < [data length]) { LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); return; } GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:maxLength timeout:timeout readLength:0 terminator:data tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { LogTrace(); if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) { [readQueue addObject:packet]; [self maybeDequeueRead]; } }}); // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } - (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr { __block float result = 0.0F; dispatch_block_t block = ^{ if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]]) { // We're not reading anything right now. if (tagPtr != NULL) *tagPtr = 0; if (donePtr != NULL) *donePtr = 0; if (totalPtr != NULL) *totalPtr = 0; result = NAN; } else { // It's only possible to know the progress of our read if we're reading to a certain length. // If we're reading to data, we of course have no idea when the data will arrive. // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. NSUInteger done = currentRead->bytesDone; NSUInteger total = currentRead->readLength; if (tagPtr != NULL) *tagPtr = currentRead->tag; if (donePtr != NULL) *donePtr = done; if (totalPtr != NULL) *totalPtr = total; if (total > 0) result = (float)done / (float)total; else result = 1.0F; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } /** * This method starts a new read, if needed. * * It is called when: * - a user requests a read * - after a read request has finished (to handle the next request) * - immediately after the socket opens to handle any pending requests * * This method also handles auto-disconnect post read/write completion. **/ - (void)maybeDequeueRead { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); // If we're not currently processing a read AND we have an available read stream if ((currentRead == nil) && (flags & kConnected)) { if ([readQueue count] > 0) { // Dequeue the next object in the write queue currentRead = [readQueue objectAtIndex:0]; [readQueue removeObjectAtIndex:0]; if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) { LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); // Attempt to start TLS flags |= kStartingReadTLS; // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set [self maybeStartTLS]; } else { LogVerbose(@"Dequeued GCDAsyncReadPacket"); // Setup read timer (if needed) [self setupReadTimerWithTimeout:currentRead->timeout]; // Immediately read, if possible [self doReadData]; } } else if (flags & kDisconnectAfterReads) { if (flags & kDisconnectAfterWrites) { if (([writeQueue count] == 0) && (currentWrite == nil)) { [self closeWithError:nil]; } } else { [self closeWithError:nil]; } } else if (flags & kSocketSecure) { [self flushSSLBuffers]; // Edge case: // // We just drained all data from the ssl buffers, // and all known data from the socket (socketFDBytesAvailable). // // If we didn't get any data from this process, // then we may have reached the end of the TCP stream. // // Be sure callbacks are enabled so we're notified about a disconnection. if ([preBuffer availableBytes] == 0) { if ([self usingCFStreamForTLS]) { // Callbacks never disabled } else { [self resumeReadSource]; } } } } } - (void)flushSSLBuffers { LogTrace(); NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); if ([preBuffer availableBytes] > 0) { // Only flush the ssl buffers if the prebuffer is empty. // This is to avoid growing the prebuffer inifinitely large. return; } #if TARGET_OS_IPHONE if ([self usingCFStreamForTLS]) { if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) { LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); CFIndex defaultBytesToRead = (1024 * 4); [preBuffer ensureCapacityForWrite:defaultBytesToRead]; uint8_t *buffer = [preBuffer writeBuffer]; CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); if (result > 0) { [preBuffer didWrite:result]; } flags &= ~kSecureSocketHasBytesAvailable; } return; } #endif __block NSUInteger estimatedBytesAvailable = 0; dispatch_block_t updateEstimatedBytesAvailable = ^{ // Figure out if there is any data available to be read // // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered // // We call the variable "estimated" because we don't know how many decrypted bytes we'll get // from the encrypted bytes in the sslPreBuffer. // However, we do know this is an upper bound on the estimation. estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes]; size_t sslInternalBufSize = 0; SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); estimatedBytesAvailable += sslInternalBufSize; }; updateEstimatedBytesAvailable(); if (estimatedBytesAvailable > 0) { LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); BOOL done = NO; do { LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); // Make sure there's enough room in the prebuffer [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; // Read data into prebuffer uint8_t *buffer = [preBuffer writeBuffer]; size_t bytesRead = 0; OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); if (bytesRead > 0) { [preBuffer didWrite:bytesRead]; } LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); if (result != noErr) { done = YES; } else { updateEstimatedBytesAvailable(); } } while (!done && estimatedBytesAvailable > 0); } } - (void)doReadData { LogTrace(); // This method is called on the socketQueue. // It might be called directly, or via the readSource when data is available to be read. if ((currentRead == nil) || (flags & kReadsPaused)) { LogVerbose(@"No currentRead or kReadsPaused"); // Unable to read at this time if (flags & kSocketSecure) { // Here's the situation: // // We have an established secure connection. // There may not be a currentRead, but there might be encrypted data sitting around for us. // When the user does get around to issuing a read, that encrypted data will need to be decrypted. // // So why make the user wait? // We might as well get a head start on decrypting some data now. // // The other reason we do this has to do with detecting a socket disconnection. // The SSL/TLS protocol has it's own disconnection handshake. // So when a secure socket is closed, a "goodbye" packet comes across the wire. // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. [self flushSSLBuffers]; } if ([self usingCFStreamForTLS]) { // CFReadStream only fires once when there is available data. // It won't fire again until we've invoked CFReadStreamRead. } else { // If the readSource is firing, we need to pause it // or else it will continue to fire over and over again. // // If the readSource is not firing, // we want it to continue monitoring the socket. if (socketFDBytesAvailable > 0) { [self suspendReadSource]; } } return; } BOOL hasBytesAvailable = NO; unsigned long estimatedBytesAvailable = 0; if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) estimatedBytesAvailable = 0; if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) hasBytesAvailable = YES; else hasBytesAvailable = NO; #endif } else { estimatedBytesAvailable = socketFDBytesAvailable; if (flags & kSocketSecure) { // There are 2 buffers to be aware of here. // // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. // // The first buffer is one we create. // SecureTransport often requests small amounts of data. // This has to do with the encypted packets that are coming across the TCP stream. // But it's non-optimal to do a bunch of small reads from the BSD socket. // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) // and may store excess in the sslPreBuffer. estimatedBytesAvailable += [sslPreBuffer availableBytes]; // The second buffer is within SecureTransport. // As mentioned earlier, there are encrypted packets coming across the TCP stream. // SecureTransport needs the entire packet to decrypt it. // But if the entire packet produces X bytes of decrypted data, // and we only asked SecureTransport for X/2 bytes of data, // it must store the extra X/2 bytes of decrypted data for the next read. // // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. // From the documentation: // // "This function does not block or cause any low-level read operations to occur." size_t sslInternalBufSize = 0; SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); estimatedBytesAvailable += sslInternalBufSize; } hasBytesAvailable = (estimatedBytesAvailable > 0); } if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) { LogVerbose(@"No data available to read..."); // No data available to read. if (![self usingCFStreamForTLS]) { // Need to wait for readSource to fire and notify us of // available data in the socket's internal read buffer. [self resumeReadSource]; } return; } if (flags & kStartingReadTLS) { LogVerbose(@"Waiting for SSL/TLS handshake to complete"); // The readQueue is waiting for SSL/TLS handshake to complete. if (flags & kStartingWriteTLS) { if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) { // We are in the process of a SSL Handshake. // We were waiting for incoming data which has just arrived. [self ssl_continueSSLHandshake]; } } else { // We are still waiting for the writeQueue to drain and start the SSL/TLS process. // We now know data is available to read. if (![self usingCFStreamForTLS]) { // Suspend the read source or else it will continue to fire nonstop. [self suspendReadSource]; } } return; } BOOL done = NO; // Completed read operation NSError *error = nil; // Error occurred NSUInteger totalBytesReadForCurrentRead = 0; // // STEP 1 - READ FROM PREBUFFER // if ([preBuffer availableBytes] > 0) { // There are 3 types of read packets: // // 1) Read all available data. // 2) Read a specific length of data. // 3) Read up to a particular terminator. NSUInteger bytesToCopy; if (currentRead->term != nil) { // Read type #3 - read up to a terminator bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; } else { // Read type #1 or #2 bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; } // Make sure we have enough room in the buffer for our read. [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; // Copy bytes from prebuffer into packet buffer uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; memcpy(buffer, [preBuffer readBuffer], bytesToCopy); // Remove the copied bytes from the preBuffer [preBuffer didRead:bytesToCopy]; LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); // Update totals currentRead->bytesDone += bytesToCopy; totalBytesReadForCurrentRead += bytesToCopy; // Check to see if the read operation is done if (currentRead->readLength > 0) { // Read type #2 - read a specific length of data done = (currentRead->bytesDone == currentRead->readLength); } else if (currentRead->term != nil) { // Read type #3 - read up to a terminator // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method if (!done && currentRead->maxLength > 0) { // We're not done and there's a set maxLength. // Have we reached that maxLength yet? if (currentRead->bytesDone >= currentRead->maxLength) { error = [self readMaxedOutError]; } } } else { // Read type #1 - read all available data // // We're done as soon as // - we've read all available data (in prebuffer and socket) // - we've read the maxLength of read packet. done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); } } // // STEP 2 - READ FROM SOCKET // BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more if (!done && !error && !socketEOF && hasBytesAvailable) { NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); BOOL readIntoPreBuffer = NO; uint8_t *buffer = NULL; size_t bytesRead = 0; if (flags & kSocketSecure) { if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE // Using CFStream, rather than SecureTransport, for TLS NSUInteger defaultReadLength = (1024 * 32); NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength shouldPreBuffer:&readIntoPreBuffer]; // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } // Read data into buffer CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); if (result < 0) { error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); } else if (result == 0) { socketEOF = YES; } else { waiting = YES; bytesRead = (size_t)result; } // We only know how many decrypted bytes were read. // The actual number of bytes read was likely more due to the overhead of the encryption. // So we reset our flag, and rely on the next callback to alert us of more data. flags &= ~kSecureSocketHasBytesAvailable; #endif } else { // Using SecureTransport for TLS // // We know: // - how many bytes are available on the socket // - how many encrypted bytes are sitting in the sslPreBuffer // - how many decypted bytes are sitting in the sslContext // // But we do NOT know: // - how many encypted bytes are sitting in the sslContext // // So we play the regular game of using an upper bound instead. NSUInteger defaultReadLength = (1024 * 32); if (defaultReadLength < estimatedBytesAvailable) { defaultReadLength = estimatedBytesAvailable + (1024 * 16); } NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength shouldPreBuffer:&readIntoPreBuffer]; if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t bytesToRead = SIZE_MAX; } // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } // The documentation from Apple states: // // "a read operation might return errSSLWouldBlock, // indicating that less data than requested was actually transferred" // // However, starting around 10.7, the function will sometimes return noErr, // even if it didn't read as much data as requested. So we need to watch out for that. OSStatus result; do { void *loop_buffer = buffer + bytesRead; size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; size_t loop_bytesRead = 0; result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); bytesRead += loop_bytesRead; } while ((result == noErr) && (bytesRead < bytesToRead)); if (result != noErr) { if (result == errSSLWouldBlock) waiting = YES; else { if (result == errSSLClosedGraceful || result == errSSLClosedAbort) { // We've reached the end of the stream. // Handle this the same way we would an EOF from the socket. socketEOF = YES; sslErrCode = result; } else { error = [self sslError:result]; } } // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. // This happens when the SSLRead function is able to read some data, // but not the entire amount we requested. if (bytesRead <= 0) { bytesRead = 0; } } // Do not modify socketFDBytesAvailable. // It will be updated via the SSLReadFunction(). } } else { // Normal socket operation NSUInteger bytesToRead; // There are 3 types of read packets: // // 1) Read all available data. // 2) Read a specific length of data. // 3) Read up to a particular terminator. if (currentRead->term != nil) { // Read type #3 - read up to a terminator bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable shouldPreBuffer:&readIntoPreBuffer]; } else { // Read type #1 or #2 bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; } if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) bytesToRead = SIZE_MAX; } // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } // Read data into buffer int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); LogVerbose(@"read from socket = %i", (int)result); if (result < 0) { if (errno == EWOULDBLOCK) waiting = YES; else error = [self errnoErrorWithReason:@"Error in read() function"]; socketFDBytesAvailable = 0; } else if (result == 0) { socketEOF = YES; socketFDBytesAvailable = 0; } else { bytesRead = result; if (bytesRead < bytesToRead) { // The read returned less data than requested. // This means socketFDBytesAvailable was a bit off due to timing, // because we read from the socket right when the readSource event was firing. socketFDBytesAvailable = 0; } else { if (socketFDBytesAvailable <= bytesRead) socketFDBytesAvailable = 0; else socketFDBytesAvailable -= bytesRead; } if (socketFDBytesAvailable == 0) { waiting = YES; } } } if (bytesRead > 0) { // Check to see if the read operation is done if (currentRead->readLength > 0) { // Read type #2 - read a specific length of data // // Note: We should never be using a prebuffer when we're reading a specific length of data. NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; done = (currentRead->bytesDone == currentRead->readLength); } else if (currentRead->term != nil) { // Read type #3 - read up to a terminator if (readIntoPreBuffer) { // We just read a big chunk of data into the preBuffer [preBuffer didWrite:bytesRead]; LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); // Search for the terminating sequence NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); // Ensure there's room on the read packet's buffer [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; // Copy bytes from prebuffer into read buffer uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); // Remove the copied bytes from the prebuffer [preBuffer didRead:bytesToCopy]; LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); // Update totals currentRead->bytesDone += bytesToCopy; totalBytesReadForCurrentRead += bytesToCopy; // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above } else { // We just read a big chunk of data directly into the packet's buffer. // We need to move any overflow into the prebuffer. NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; if (overflow == 0) { // Perfect match! // Every byte we read stays in the read buffer, // and the last byte we read was the last byte of the term. currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; done = YES; } else if (overflow > 0) { // The term was found within the data that we read, // and there are extra bytes that extend past the end of the term. // We need to move these excess bytes out of the read packet and into the prebuffer. NSInteger underflow = bytesRead - overflow; // Copy excess data into preBuffer LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); [preBuffer ensureCapacityForWrite:overflow]; uint8_t *overflowBuffer = buffer + underflow; memcpy([preBuffer writeBuffer], overflowBuffer, overflow); [preBuffer didWrite:overflow]; LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); // Note: The completeCurrentRead method will trim the buffer for us. currentRead->bytesDone += underflow; totalBytesReadForCurrentRead += underflow; done = YES; } else { // The term was not found within the data that we read. currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; done = NO; } } if (!done && currentRead->maxLength > 0) { // We're not done and there's a set maxLength. // Have we reached that maxLength yet? if (currentRead->bytesDone >= currentRead->maxLength) { error = [self readMaxedOutError]; } } } else { // Read type #1 - read all available data if (readIntoPreBuffer) { // We just read a chunk of data into the preBuffer [preBuffer didWrite:bytesRead]; // Now copy the data into the read packet. // // Recall that we didn't read directly into the packet's buffer to avoid // over-allocating memory since we had no clue how much data was available to be read. // // Ensure there's room on the read packet's buffer [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; // Copy bytes from prebuffer into read buffer uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; memcpy(readBuf, [preBuffer readBuffer], bytesRead); // Remove the copied bytes from the prebuffer [preBuffer didRead:bytesRead]; // Update totals currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; } else { currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; } done = YES; } } // if (bytesRead > 0) } // if (!done && !error && !socketEOF && hasBytesAvailable) if (!done && currentRead->readLength == 0 && currentRead->term == nil) { // Read type #1 - read all available data // // We might arrive here if we read data from the prebuffer but not from the socket. done = (totalBytesReadForCurrentRead > 0); } // Check to see if we're done, or if we've made progress if (done) { [self completeCurrentRead]; if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) { [self maybeDequeueRead]; } } else if (totalBytesReadForCurrentRead > 0) { // We're not done read type #2 or #3 yet, but we have read in some bytes __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) { long theReadTag = currentRead->tag; dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; }}); } } // Check for errors if (error) { [self closeWithError:error]; } else if (socketEOF) { [self doReadEOF]; } else if (waiting) { if (![self usingCFStreamForTLS]) { // Monitor the socket for readability (if we're not already doing so) [self resumeReadSource]; } } // Do not add any code here without first adding return statements in the error cases above. } - (void)doReadEOF { LogTrace(); // This method may be called more than once. // If the EOF is read while there is still data in the preBuffer, // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. flags |= kSocketHasReadEOF; if (flags & kSocketSecure) { // If the SSL layer has any buffered data, flush it into the preBuffer now. [self flushSSLBuffers]; } BOOL shouldDisconnect = NO; NSError *error = nil; if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) { // We received an EOF during or prior to startTLS. // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. shouldDisconnect = YES; if ([self usingSecureTransportForTLS]) { error = [self sslError:errSSLClosedAbort]; } } else if (flags & kReadStreamClosed) { // The preBuffer has already been drained. // The config allows half-duplex connections. // We've previously checked the socket, and it appeared writeable. // So we marked the read stream as closed and notified the delegate. // // As per the half-duplex contract, the socket will be closed when a write fails, // or when the socket is manually closed. shouldDisconnect = NO; } else if ([preBuffer availableBytes] > 0) { LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); // Although we won't be able to read any more data from the socket, // there is existing data that has been prebuffered that we can read. shouldDisconnect = NO; } else if (config & kAllowHalfDuplexConnection) { // We just received an EOF (end of file) from the socket's read stream. // This means the remote end of the socket (the peer we're connected to) // has explicitly stated that it will not be sending us any more data. // // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) #if GCDAsyncSocketReadEOFPollsSocketForWriteStatus == 1 int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; struct pollfd pfd[1]; pfd[0].fd = socketFD; pfd[0].events = POLLOUT; pfd[0].revents = 0; poll(pfd, 1, 0); if (pfd[0].revents & POLLOUT) { // Socket appears to still be writeable #endif shouldDisconnect = NO; flags |= kReadStreamClosed; // Notify the delegate that we're going half-duplex __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidCloseReadStream:self]; }}); } #if GCDAsyncSocketReadEOFPollsSocketForWriteStatus == 1 } else { shouldDisconnect = YES; } #endif } else { shouldDisconnect = YES; } if (shouldDisconnect) { if (error == nil) { if ([self usingSecureTransportForTLS]) { if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) { error = [self sslError:sslErrCode]; } else { error = [self connectionClosedError]; } } else { error = [self connectionClosedError]; } } [self closeWithError:error]; } else { if (![self usingCFStreamForTLS]) { // Suspend the read source (if needed) [self suspendReadSource]; } } } - (void)completeCurrentRead { LogTrace(); NSAssert(currentRead, @"Trying to complete current read when there is no current read."); NSData *result = nil; if (currentRead->bufferOwner) { // We created the buffer on behalf of the user. // Trim our buffer to be the proper size. [currentRead->buffer setLength:currentRead->bytesDone]; result = currentRead->buffer; } else { // We did NOT create the buffer. // The buffer is owned by the caller. // Only trim the buffer if we had to increase its size. if ([currentRead->buffer length] > currentRead->originalBufferLength) { NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; NSUInteger origSize = currentRead->originalBufferLength; NSUInteger buffSize = MAX(readSize, origSize); [currentRead->buffer setLength:buffSize]; } uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; } __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) { GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didReadData:result withTag:theRead->tag]; }}); } [self endCurrentRead]; } - (void)endCurrentRead { if (readTimer) { dispatch_source_cancel(readTimer); readTimer = NULL; } currentRead = nil; } - (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { unsigned long readTimerMask = 0; if (config & kUseStrictTimers) { readTimerMask = DISPATCH_TIMER_STRICT; } readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, readTimerMask, socketQueue); __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; [strongSelf doReadTimeout]; #pragma clang diagnostic pop }}); #if !OS_OBJECT_USE_OBJC dispatch_source_t theReadTimer = readTimer; dispatch_source_set_cancel_handler(readTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"dispatch_release(readTimer)"); dispatch_release(theReadTimer); #pragma clang diagnostic pop }); #endif dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(readTimer); } } - (void)doReadTimeout { // This is a little bit tricky. // Ideally we'd like to synchronously query the delegate about a timeout extension. // But if we do so synchronously we risk a possible deadlock. // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. flags |= kReadsPaused; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) { GCDAsyncReadPacket *theRead = currentRead; dispatch_async(delegateQueue, ^{ @autoreleasepool { NSTimeInterval timeoutExtension = 0.0; timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag elapsed:theRead->timeout bytesDone:theRead->bytesDone]; dispatch_async(socketQueue, ^{ @autoreleasepool { [self doReadTimeoutWithExtension:timeoutExtension]; }}); }}); } else { [self doReadTimeoutWithExtension:0.0]; } } - (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension { if (currentRead) { if (timeoutExtension > 0.0) { currentRead->timeout += timeoutExtension; // Reschedule the timer dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); // Unpause reads, and continue flags &= ~kReadsPaused; [self doReadData]; } else { LogVerbose(@"ReadTimeout"); [self closeWithError:[self readTimeoutError]]; } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Writing //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { if ([data length] == 0) return; GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { LogTrace(); if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) { [writeQueue addObject:packet]; [self maybeDequeueWrite]; } }}); // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } - (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr { __block float result = 0.0F; dispatch_block_t block = ^{ if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) { // We're not writing anything right now. if (tagPtr != NULL) *tagPtr = 0; if (donePtr != NULL) *donePtr = 0; if (totalPtr != NULL) *totalPtr = 0; result = NAN; } else { NSUInteger done = currentWrite->bytesDone; NSUInteger total = [currentWrite->buffer length]; if (tagPtr != NULL) *tagPtr = currentWrite->tag; if (donePtr != NULL) *donePtr = done; if (totalPtr != NULL) *totalPtr = total; result = (float)done / (float)total; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } /** * Conditionally starts a new write. * * It is called when: * - a user requests a write * - after a write request has finished (to handle the next request) * - immediately after the socket opens to handle any pending requests * * This method also handles auto-disconnect post read/write completion. **/ - (void)maybeDequeueWrite { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); // If we're not currently processing a write AND we have an available write stream if ((currentWrite == nil) && (flags & kConnected)) { if ([writeQueue count] > 0) { // Dequeue the next object in the write queue currentWrite = [writeQueue objectAtIndex:0]; [writeQueue removeObjectAtIndex:0]; if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) { LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); // Attempt to start TLS flags |= kStartingWriteTLS; // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set [self maybeStartTLS]; } else { LogVerbose(@"Dequeued GCDAsyncWritePacket"); // Setup write timer (if needed) [self setupWriteTimerWithTimeout:currentWrite->timeout]; // Immediately write, if possible [self doWriteData]; } } else if (flags & kDisconnectAfterWrites) { if (flags & kDisconnectAfterReads) { if (([readQueue count] == 0) && (currentRead == nil)) { [self closeWithError:nil]; } } else { [self closeWithError:nil]; } } } } - (void)doWriteData { LogTrace(); // This method is called by the writeSource via the socketQueue if ((currentWrite == nil) || (flags & kWritesPaused)) { LogVerbose(@"No currentWrite or kWritesPaused"); // Unable to write at this time if ([self usingCFStreamForTLS]) { // CFWriteStream only fires once when there is available data. // It won't fire again until we've invoked CFWriteStreamWrite. } else { // If the writeSource is firing, we need to pause it // or else it will continue to fire over and over again. if (flags & kSocketCanAcceptBytes) { [self suspendWriteSource]; } } return; } if (!(flags & kSocketCanAcceptBytes)) { LogVerbose(@"No space available to write..."); // No space available to write. if (![self usingCFStreamForTLS]) { // Need to wait for writeSource to fire and notify us of // available space in the socket's internal write buffer. [self resumeWriteSource]; } return; } if (flags & kStartingWriteTLS) { LogVerbose(@"Waiting for SSL/TLS handshake to complete"); // The writeQueue is waiting for SSL/TLS handshake to complete. if (flags & kStartingReadTLS) { if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) { // We are in the process of a SSL Handshake. // We were waiting for available space in the socket's internal OS buffer to continue writing. [self ssl_continueSSLHandshake]; } } else { // We are still waiting for the readQueue to drain and start the SSL/TLS process. // We now know we can write to the socket. if (![self usingCFStreamForTLS]) { // Suspend the write source or else it will continue to fire nonstop. [self suspendWriteSource]; } } return; } // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) BOOL waiting = NO; NSError *error = nil; size_t bytesWritten = 0; if (flags & kSocketSecure) { if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE // // Writing data using CFStream (over internal TLS) // const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); if (result < 0) { error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); } else { bytesWritten = (size_t)result; // We always set waiting to true in this scenario. // CFStream may have altered our underlying socket to non-blocking. // Thus if we attempt to write without a callback, we may end up blocking our queue. waiting = YES; } #endif } else { // We're going to use the SSLWrite function. // // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) // // Parameters: // context - An SSL session context reference. // data - A pointer to the buffer of data to write. // dataLength - The amount, in bytes, of data to write. // processed - On return, the length, in bytes, of the data actually written. // // It sounds pretty straight-forward, // but there are a few caveats you should be aware of. // // The SSLWrite method operates in a non-obvious (and rather annoying) manner. // According to the documentation: // // Because you may configure the underlying connection to operate in a non-blocking manner, // a write operation might return errSSLWouldBlock, indicating that less data than requested // was actually transferred. In this case, you should repeat the call to SSLWrite until some // other result is returned. // // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), // but it sets processed to dataLength !! // // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. // // You might be wondering: // If the SSLWrite function doesn't tell us how many bytes were written, // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) // for the next time we invoke SSLWrite? // // The answer is that SSLWrite cached all the data we told it to write, // and it will push out that data next time we call SSLWrite. // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. // If we call SSLWrite with empty data, then it will simply push out the cached data. // // For this purpose we're going to break large writes into a series of smaller writes. // This allows us to report progress back to the delegate. OSStatus result; BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); BOOL hasNewDataToWrite = YES; if (hasCachedDataToWrite) { size_t processed = 0; result = SSLWrite(sslContext, NULL, 0, &processed); if (result == noErr) { bytesWritten = sslWriteCachedLength; sslWriteCachedLength = 0; if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) { // We've written all data for the current write. hasNewDataToWrite = NO; } } else { if (result == errSSLWouldBlock) { waiting = YES; } else { error = [self sslError:result]; } // Can't write any new data since we were unable to write the cached data. hasNewDataToWrite = NO; } } if (hasNewDataToWrite) { const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone + bytesWritten; NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } size_t bytesRemaining = bytesToWrite; BOOL keepLooping = YES; while (keepLooping) { const size_t sslMaxBytesToWrite = 32768; size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); size_t sslBytesWritten = 0; result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); if (result == noErr) { buffer += sslBytesWritten; bytesWritten += sslBytesWritten; bytesRemaining -= sslBytesWritten; keepLooping = (bytesRemaining > 0); } else { if (result == errSSLWouldBlock) { waiting = YES; sslWriteCachedLength = sslBytesToWrite; } else { error = [self sslError:result]; } keepLooping = NO; } } // while (keepLooping) } // if (hasNewDataToWrite) } } else { // // Writing data directly over raw socket // int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); LogVerbose(@"wrote to socket = %zd", result); // Check results if (result < 0) { if (errno == EWOULDBLOCK) { waiting = YES; } else { error = [self errnoErrorWithReason:@"Error in write() function"]; } } else { bytesWritten = result; } } // We're done with our writing. // If we explictly ran into a situation where the socket told us there was no room in the buffer, // then we immediately resume listening for notifications. // // We must do this before we dequeue another write, // as that may in turn invoke this method again. // // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. if (waiting) { flags &= ~kSocketCanAcceptBytes; if (![self usingCFStreamForTLS]) { [self resumeWriteSource]; } } // Check our results BOOL done = NO; if (bytesWritten > 0) { // Update total amount read for the current write currentWrite->bytesDone += bytesWritten; LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); // Is packet done? done = (currentWrite->bytesDone == [currentWrite->buffer length]); } if (done) { [self completeCurrentWrite]; if (!error) { dispatch_async(socketQueue, ^{ @autoreleasepool{ [self maybeDequeueWrite]; }}); } } else { // We were unable to finish writing the data, // so we're waiting for another callback to notify us of available space in the lower-level output buffer. if (!waiting && !error) { // This would be the case if our write was able to accept some data, but not all of it. flags &= ~kSocketCanAcceptBytes; if (![self usingCFStreamForTLS]) { [self resumeWriteSource]; } } if (bytesWritten > 0) { // We're not done with the entire write, but we have written some bytes __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) { long theWriteTag = currentWrite->tag; dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; }}); } } } // Check for errors if (error) { [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]]; } // Do not add any code here without first adding a return statement in the error case above. } - (void)completeCurrentWrite { LogTrace(); NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) { long theWriteTag = currentWrite->tag; dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didWriteDataWithTag:theWriteTag]; }}); } [self endCurrentWrite]; } - (void)endCurrentWrite { if (writeTimer) { dispatch_source_cancel(writeTimer); writeTimer = NULL; } currentWrite = nil; } - (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { unsigned long writeTimerMask = 0; if (config & kUseStrictTimers) { writeTimerMask = DISPATCH_TIMER_STRICT; } writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, writeTimerMask, socketQueue); __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; [strongSelf doWriteTimeout]; #pragma clang diagnostic pop }}); #if !OS_OBJECT_USE_OBJC dispatch_source_t theWriteTimer = writeTimer; dispatch_source_set_cancel_handler(writeTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"dispatch_release(writeTimer)"); dispatch_release(theWriteTimer); #pragma clang diagnostic pop }); #endif dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(writeTimer); } } - (void)doWriteTimeout { // This is a little bit tricky. // Ideally we'd like to synchronously query the delegate about a timeout extension. // But if we do so synchronously we risk a possible deadlock. // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. flags |= kWritesPaused; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) { GCDAsyncWritePacket *theWrite = currentWrite; dispatch_async(delegateQueue, ^{ @autoreleasepool { NSTimeInterval timeoutExtension = 0.0; timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag elapsed:theWrite->timeout bytesDone:theWrite->bytesDone]; dispatch_async(socketQueue, ^{ @autoreleasepool { [self doWriteTimeoutWithExtension:timeoutExtension]; }}); }}); } else { [self doWriteTimeoutWithExtension:0.0]; } } - (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension { if (currentWrite) { if (timeoutExtension > 0.0) { currentWrite->timeout += timeoutExtension; // Reschedule the timer dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); // Unpause writes, and continue flags &= ~kWritesPaused; [self doWriteData]; } else { LogVerbose(@"WriteTimeout"); [self closeWithError:[self writeTimeoutError]]; } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Security //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)startTLS:(NSDictionary *)tlsSettings { LogTrace(); if (tlsSettings == nil) { // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, // but causes problems if we later try to fetch the remote host's certificate. // // To be exact, it causes the following to return NULL instead of the normal result: // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) // // So we use an empty dictionary instead, which works perfectly. tlsSettings = [NSDictionary dictionary]; } GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; dispatch_async(socketQueue, ^{ @autoreleasepool { if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites)) { [readQueue addObject:packet]; [writeQueue addObject:packet]; flags |= kQueuedTLS; [self maybeDequeueRead]; [self maybeDequeueWrite]; } }}); } - (void)maybeStartTLS { // We can't start TLS until: // - All queued reads prior to the user calling startTLS are complete // - All queued writes prior to the user calling startTLS are complete // // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { BOOL useSecureTransport = YES; #if TARGET_OS_IPHONE { GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; NSDictionary *tlsSettings = @{}; if (tlsPacket) { tlsSettings = tlsPacket->tlsSettings; } NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; if (value && [value boolValue]) useSecureTransport = NO; } #endif if (useSecureTransport) { [self ssl_startTLS]; } else { #if TARGET_OS_IPHONE [self cf_startTLS]; #endif } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Security via SecureTransport //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength { LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) { LogVerbose(@"%@ - No data available to read...", THIS_METHOD); // No data available to read. // // Need to wait for readSource to fire and notify us of // available data in the socket's internal read buffer. [self resumeReadSource]; *bufferLength = 0; return errSSLWouldBlock; } size_t totalBytesRead = 0; size_t totalBytesLeftToBeRead = *bufferLength; BOOL done = NO; BOOL socketError = NO; // // STEP 1 : READ FROM SSL PRE BUFFER // size_t sslPreBufferLength = [sslPreBuffer availableBytes]; if (sslPreBufferLength > 0) { LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); size_t bytesToCopy; if (sslPreBufferLength > totalBytesLeftToBeRead) bytesToCopy = totalBytesLeftToBeRead; else bytesToCopy = sslPreBufferLength; LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); [sslPreBuffer didRead:bytesToCopy]; LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); totalBytesRead += bytesToCopy; totalBytesLeftToBeRead -= bytesToCopy; done = (totalBytesLeftToBeRead == 0); if (done) LogVerbose(@"%@: Complete", THIS_METHOD); } // // STEP 2 : READ FROM SOCKET // if (!done && (socketFDBytesAvailable > 0)) { LogVerbose(@"%@: Reading from socket...", THIS_METHOD); int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; BOOL readIntoPreBuffer; size_t bytesToRead; uint8_t *buf; if (socketFDBytesAvailable > totalBytesLeftToBeRead) { // Read all available data from socket into sslPreBuffer. // Then copy requested amount into dataBuffer. LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; readIntoPreBuffer = YES; bytesToRead = (size_t)socketFDBytesAvailable; buf = [sslPreBuffer writeBuffer]; } else { // Read available data from socket directly into dataBuffer. LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); readIntoPreBuffer = NO; bytesToRead = totalBytesLeftToBeRead; buf = (uint8_t *)buffer + totalBytesRead; } ssize_t result = read(socketFD, buf, bytesToRead); LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); if (result < 0) { LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); if (errno != EWOULDBLOCK) { socketError = YES; } socketFDBytesAvailable = 0; } else if (result == 0) { LogVerbose(@"%@: read EOF", THIS_METHOD); socketError = YES; socketFDBytesAvailable = 0; } else { size_t bytesReadFromSocket = result; if (socketFDBytesAvailable > bytesReadFromSocket) socketFDBytesAvailable -= bytesReadFromSocket; else socketFDBytesAvailable = 0; if (readIntoPreBuffer) { [sslPreBuffer didWrite:bytesReadFromSocket]; size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); [sslPreBuffer didRead:bytesToCopy]; totalBytesRead += bytesToCopy; totalBytesLeftToBeRead -= bytesToCopy; LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); } else { totalBytesRead += bytesReadFromSocket; totalBytesLeftToBeRead -= bytesReadFromSocket; } done = (totalBytesLeftToBeRead == 0); if (done) LogVerbose(@"%@: Complete", THIS_METHOD); } } *bufferLength = totalBytesRead; if (done) return noErr; if (socketError) return errSSLClosedAbort; return errSSLWouldBlock; } - (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength { if (!(flags & kSocketCanAcceptBytes)) { // Unable to write. // // Need to wait for writeSource to fire and notify us of // available space in the socket's internal write buffer. [self resumeWriteSource]; *bufferLength = 0; return errSSLWouldBlock; } size_t bytesToWrite = *bufferLength; size_t bytesWritten = 0; BOOL done = NO; BOOL socketError = NO; int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; ssize_t result = write(socketFD, buffer, bytesToWrite); if (result < 0) { if (errno != EWOULDBLOCK) { socketError = YES; } flags &= ~kSocketCanAcceptBytes; } else if (result == 0) { flags &= ~kSocketCanAcceptBytes; } else { bytesWritten = result; done = (bytesWritten == bytesToWrite); } *bufferLength = bytesWritten; if (done) return noErr; if (socketError) return errSSLClosedAbort; return errSSLWouldBlock; } static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); return [asyncSocket sslReadWithBuffer:data length:dataLength]; } static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); return [asyncSocket sslWriteWithBuffer:data length:dataLength]; } - (void)ssl_startTLS { LogTrace(); LogVerbose(@"Starting TLS (via SecureTransport)..."); OSStatus status; GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; if (tlsPacket == nil) // Code to quiet the analyzer { NSAssert(NO, @"Logic error"); [self closeWithError:[self otherError:@"Logic error"]]; return; } NSDictionary *tlsSettings = tlsPacket->tlsSettings; // Create SSLContext, and setup IO callbacks and connection ref BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue]; #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) { if (isServer) sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); else sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); if (sslContext == NULL) { [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; return; } } #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) { status = SSLNewContext(isServer, &sslContext); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; return; } } #endif status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; return; } status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; return; } BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue]; if (shouldManuallyEvaluateTrust) { if (isServer) { [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; return; } status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; return; } #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) // Note from Apple's documentation: // // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus // SSLSetEnableCertVerify is not available on that platform at all. status = SSLSetEnableCertVerify(sslContext, NO); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; return; } #endif } // Configure SSLContext from given settings // // Checklist: // 1. kCFStreamSSLPeerName // 2. kCFStreamSSLCertificates // 3. GCDAsyncSocketSSLPeerID // 4. GCDAsyncSocketSSLProtocolVersionMin // 5. GCDAsyncSocketSSLProtocolVersionMax // 6. GCDAsyncSocketSSLSessionOptionFalseStart // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord // 8. GCDAsyncSocketSSLCipherSuites // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) // // Deprecated (throw error): // 10. kCFStreamSSLAllowsAnyRoot // 11. kCFStreamSSLAllowsExpiredRoots // 12. kCFStreamSSLAllowsExpiredCertificates // 13. kCFStreamSSLValidatesCertificateChain // 14. kCFStreamSSLLevel id value; // 1. kCFStreamSSLPeerName value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; if ([value isKindOfClass:[NSString class]]) { NSString *peerName = (NSString *)value; const char *peer = [peerName UTF8String]; size_t peerLen = strlen(peer); status = SSLSetPeerDomainName(sslContext, peer, peerLen); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; return; } // 2. kCFStreamSSLCertificates value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; if ([value isKindOfClass:[NSArray class]]) { CFArrayRef certs = (__bridge CFArrayRef)value; status = SSLSetCertificate(sslContext, certs); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; return; } // 3. GCDAsyncSocketSSLPeerID value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; if ([value isKindOfClass:[NSData class]]) { NSData *peerIdData = (NSData *)value; status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." @" (You can convert strings to data using a method like" @" [string dataUsingEncoding:NSUTF8StringEncoding])"); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; return; } // 4. GCDAsyncSocketSSLProtocolVersionMin value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; if ([value isKindOfClass:[NSNumber class]]) { SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; if (minProtocol != kSSLProtocolUnknown) { status = SSLSetProtocolVersionMin(sslContext, minProtocol); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; return; } } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; return; } // 5. GCDAsyncSocketSSLProtocolVersionMax value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; if ([value isKindOfClass:[NSNumber class]]) { SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; if (maxProtocol != kSSLProtocolUnknown) { status = SSLSetProtocolVersionMax(sslContext, maxProtocol); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; return; } } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; return; } // 6. GCDAsyncSocketSSLSessionOptionFalseStart value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; if ([value isKindOfClass:[NSNumber class]]) { status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [value boolValue]); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; return; } // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; if ([value isKindOfClass:[NSNumber class]]) { status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [value boolValue]); if (status != noErr) { [self closeWithError: [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." @" Value must be of type NSNumber."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; return; } // 8. GCDAsyncSocketSSLCipherSuites value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; if ([value isKindOfClass:[NSArray class]]) { NSArray *cipherSuites = (NSArray *)value; NSUInteger numberCiphers = [cipherSuites count]; SSLCipherSuite ciphers[numberCiphers]; NSUInteger cipherIndex; for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) { NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; ciphers[cipherIndex] = [cipherObject intValue]; } status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; return; } // 9. GCDAsyncSocketSSLDiffieHellmanParameters #if !TARGET_OS_IPHONE value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; if ([value isKindOfClass:[NSData class]]) { NSData *diffieHellmanData = (NSData *)value; status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; return; } #endif // DEPRECATED checks // 10. kCFStreamSSLAllowsAnyRoot #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" @" - You must use manual trust evaluation"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; return; } // 11. kCFStreamSSLAllowsExpiredRoots #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" @" - You must use manual trust evaluation"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; return; } // 12. kCFStreamSSLValidatesCertificateChain #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" @" - You must use manual trust evaluation"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; return; } // 13. kCFStreamSSLAllowsExpiredCertificates #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" @" - You must use manual trust evaluation"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; return; } // 14. kCFStreamSSLLevel #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; return; } // Setup the sslPreBuffer // // Any data in the preBuffer needs to be moved into the sslPreBuffer, // as this data is now part of the secure read stream. sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; size_t preBufferLength = [preBuffer availableBytes]; if (preBufferLength > 0) { [sslPreBuffer ensureCapacityForWrite:preBufferLength]; memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); [preBuffer didRead:preBufferLength]; [sslPreBuffer didWrite:preBufferLength]; } sslErrCode = lastSSLHandshakeError = noErr; // Start the SSL Handshake process [self ssl_continueSSLHandshake]; } - (void)ssl_continueSSLHandshake { LogTrace(); // If the return value is noErr, the session is ready for normal secure communication. // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the // server and then call SSLHandshake again to resume the handshake or close the connection // errSSLPeerBadCert SSL error. // Otherwise, the return value indicates an error code. OSStatus status = SSLHandshake(sslContext); lastSSLHandshakeError = status; if (status == noErr) { LogVerbose(@"SSLHandshake complete"); flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; flags |= kSocketSecure; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidSecure:self]; }}); } [self endCurrentRead]; [self endCurrentWrite]; [self maybeDequeueRead]; [self maybeDequeueWrite]; } else if (status == errSSLPeerAuthCompleted) { LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); __block SecTrustRef trust = NULL; status = SSLCopyPeerTrust(sslContext, &trust); if (status != noErr) { [self closeWithError:[self sslError:status]]; return; } int aStateIndex = stateIndex; dispatch_queue_t theSocketQueue = socketQueue; __weak GCDAsyncSocket *weakSelf = self; void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" dispatch_async(theSocketQueue, ^{ @autoreleasepool { if (trust) { CFRelease(trust); trust = NULL; } __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf) { [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; } }}); #pragma clang diagnostic pop }}; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; }}); } else { if (trust) { CFRelease(trust); trust = NULL; } NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," @" but delegate doesn't implement socket:shouldTrustPeer:"; [self closeWithError:[self otherError:msg]]; return; } } else if (status == errSSLWouldBlock) { LogVerbose(@"SSLHandshake continues..."); // Handshake continues... // // This method will be called again from doReadData or doWriteData. } else { [self closeWithError:[self sslError:status]]; } } - (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex { LogTrace(); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); // One of the following is true // - the socket was disconnected // - the startTLS operation timed out // - the completionHandler was already invoked once return; } // Increment stateIndex to ensure completionHandler can only be called once. stateIndex++; if (shouldTrust) { NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError); [self ssl_continueSSLHandshake]; } else { [self closeWithError:[self sslError:errSSLPeerBadCert]]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Security via CFStream //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if TARGET_OS_IPHONE - (void)cf_finishSSLHandshake { LogTrace(); if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; flags |= kSocketSecure; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidSecure:self]; }}); } [self endCurrentRead]; [self endCurrentWrite]; [self maybeDequeueRead]; [self maybeDequeueWrite]; } } - (void)cf_abortSSLHandshake:(NSError *)error { LogTrace(); if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; [self closeWithError:error]; } } - (void)cf_startTLS { LogTrace(); LogVerbose(@"Starting TLS (via CFStream)..."); if ([preBuffer availableBytes] > 0) { NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; [self closeWithError:[self otherError:msg]]; return; } [self suspendReadSource]; [self suspendWriteSource]; socketFDBytesAvailable = 0; flags &= ~kSocketCanAcceptBytes; flags &= ~kSecureSocketHasBytesAvailable; flags |= kUsingCFStreamForTLS; if (![self createReadAndWriteStream]) { [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; return; } if (![self registerForStreamCallbacksIncludingReadWrite:YES]) { [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; return; } if (![self addStreamsToRunLoop]) { [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; return; } NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; // Getting an error concerning kCFStreamPropertySSLSettings ? // You need to add the CFNetwork framework to your iOS application. BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); // For some reason, starting around the time of iOS 4.3, // the first call to set the kCFStreamPropertySSLSettings will return true, // but the second will return false. // // Order doesn't seem to matter. // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. // Either way, the first call will return true, and the second returns false. // // Interestingly, this doesn't seem to affect anything. // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) // setting it on one side of the stream automatically sets it for the other side of the stream. // // Although there isn't anything in the documentation to suggest that the second attempt would fail. // // Furthermore, this only seems to affect streams that are negotiating a security upgrade. // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure // connection, and then a startTLS is issued. // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. { [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; return; } if (![self openStreams]) { [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; return; } LogVerbose(@"Waiting for SSL Handshake to complete..."); } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark CFStream //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if TARGET_OS_IPHONE + (void)ignore:(id)_ {} + (void)startCFStreamThreadIfNeeded { LogTrace(); static dispatch_once_t predicate; dispatch_once(&predicate, ^{ cfstreamThreadRetainCount = 0; cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); }); dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { if (++cfstreamThreadRetainCount == 1) { cfstreamThread = [[NSThread alloc] initWithTarget:self selector:@selector(cfstreamThread) object:nil]; [cfstreamThread start]; } }}); } + (void)stopCFStreamThreadIfNeeded { LogTrace(); // The creation of the cfstreamThread is relatively expensive. // So we'd like to keep it available for recycling. // However, there's a tradeoff here, because it shouldn't remain alive forever. // So what we're going to do is use a little delay before taking it down. // This way it can be reused properly in situations where multiple sockets are continually in flux. int delayInSeconds = 30; dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" if (cfstreamThreadRetainCount == 0) { LogWarn(@"Logic error concerning cfstreamThread start / stop"); return_from_block; } if (--cfstreamThreadRetainCount == 0) { [cfstreamThread cancel]; // set isCancelled flag // wake up the thread [[self class] performSelector:@selector(ignore:) onThread:cfstreamThread withObject:[NSNull null] waitUntilDone:NO]; cfstreamThread = nil; } #pragma clang diagnostic pop }}); } + (void)cfstreamThread { @autoreleasepool { [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; LogInfo(@"CFStreamThread: Started"); // We can't run the run loop unless it has an associated input source or a timer. // So we'll just create a timer that will never fire - unless the server runs for decades. [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] target:self selector:@selector(ignore:) userInfo:nil repeats:YES]; NSThread *currentThread = [NSThread currentThread]; NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; BOOL isCancelled = [currentThread isCancelled]; while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { isCancelled = [currentThread isCancelled]; } LogInfo(@"CFStreamThread: Stopped"); }} + (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket { LogTrace(); NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); CFRunLoopRef runLoop = CFRunLoopGetCurrent(); if (asyncSocket->readStream) CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); if (asyncSocket->writeStream) CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); } + (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket { LogTrace(); NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); CFRunLoopRef runLoop = CFRunLoopGetCurrent(); if (asyncSocket->readStream) CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); if (asyncSocket->writeStream) CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); } static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; switch(type) { case kCFStreamEventHasBytesAvailable: { dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); if (asyncSocket->readStream != stream) return_from_block; if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. // (A callback related to the tcp stream, but not to the SSL layer). if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) { asyncSocket->flags |= kSecureSocketHasBytesAvailable; [asyncSocket cf_finishSSLHandshake]; } } else { asyncSocket->flags |= kSecureSocketHasBytesAvailable; [asyncSocket doReadData]; } }}); break; } default: { NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); if (error == nil && type == kCFStreamEventEndEncountered) { error = [asyncSocket connectionClosedError]; } dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFReadStreamCallback - Other"); if (asyncSocket->readStream != stream) return_from_block; if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { [asyncSocket cf_abortSSLHandshake:error]; } else { [asyncSocket closeWithError:error]; } }}); break; } } } static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; switch(type) { case kCFStreamEventCanAcceptBytes: { dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); if (asyncSocket->writeStream != stream) return_from_block; if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. // (A callback related to the tcp stream, but not to the SSL layer). if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) { asyncSocket->flags |= kSocketCanAcceptBytes; [asyncSocket cf_finishSSLHandshake]; } } else { asyncSocket->flags |= kSocketCanAcceptBytes; [asyncSocket doWriteData]; } }}); break; } default: { NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); if (error == nil && type == kCFStreamEventEndEncountered) { error = [asyncSocket connectionClosedError]; } dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFWriteStreamCallback - Other"); if (asyncSocket->writeStream != stream) return_from_block; if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { [asyncSocket cf_abortSSLHandshake:error]; } else { [asyncSocket closeWithError:error]; } }}); break; } } } - (BOOL)createReadAndWriteStream { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (readStream || writeStream) { // Streams already created return YES; } int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; if (socketFD == SOCKET_NULL) { // Cannot create streams without a file descriptor return NO; } if (![self isConnected]) { // Cannot create streams until file descriptor is connected return NO; } LogVerbose(@"Creating read and write stream..."); CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). // But let's not take any chances. if (readStream) CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); if (writeStream) CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); if ((readStream == NULL) || (writeStream == NULL)) { LogWarn(@"Unable to create read and write stream..."); if (readStream) { CFReadStreamClose(readStream); CFRelease(readStream); readStream = NULL; } if (writeStream) { CFWriteStreamClose(writeStream); CFRelease(writeStream); writeStream = NULL; } return NO; } return YES; } - (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite { LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); streamContext.version = 0; streamContext.info = (__bridge void *)(self); streamContext.retain = nil; streamContext.release = nil; streamContext.copyDescription = nil; CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; if (includeReadWrite) readStreamEvents |= kCFStreamEventHasBytesAvailable; if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) { return NO; } CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; if (includeReadWrite) writeStreamEvents |= kCFStreamEventCanAcceptBytes; if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) { return NO; } return YES; } - (BOOL)addStreamsToRunLoop { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); if (!(flags & kAddedStreamsToRunLoop)) { LogVerbose(@"Adding streams to runloop..."); [[self class] startCFStreamThreadIfNeeded]; dispatch_sync(cfstreamThreadSetupQueue, ^{ [[self class] performSelector:@selector(scheduleCFStreams:) onThread:cfstreamThread withObject:self waitUntilDone:YES]; }); flags |= kAddedStreamsToRunLoop; } return YES; } - (void)removeStreamsFromRunLoop { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); if (flags & kAddedStreamsToRunLoop) { LogVerbose(@"Removing streams from runloop..."); dispatch_sync(cfstreamThreadSetupQueue, ^{ [[self class] performSelector:@selector(unscheduleCFStreams:) onThread:cfstreamThread withObject:self waitUntilDone:YES]; }); [[self class] stopCFStreamThreadIfNeeded]; flags &= ~kAddedStreamsToRunLoop; } } - (BOOL)openStreams { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) { LogVerbose(@"Opening read and write stream..."); BOOL r1 = CFReadStreamOpen(readStream); BOOL r2 = CFWriteStreamOpen(writeStream); if (!r1 || !r2) { LogError(@"Error in CFStreamOpen"); return NO; } } return YES; } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Advanced //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * See header file for big discussion of this method. **/ - (BOOL)autoDisconnectOnClosedReadStream { // Note: YES means kAllowHalfDuplexConnection is OFF if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kAllowHalfDuplexConnection) == 0); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = ((config & kAllowHalfDuplexConnection) == 0); }); return result; } } /** * See header file for big discussion of this method. **/ - (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag { // Note: YES means kAllowHalfDuplexConnection is OFF dispatch_block_t block = ^{ if (flag) config &= ~kAllowHalfDuplexConnection; else config |= kAllowHalfDuplexConnection; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } /** * See header file for big discussion of this method. **/ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue { void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); } /** * See header file for big discussion of this method. **/ - (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue { dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); } /** * See header file for big discussion of this method. **/ - (void)performBlock:(dispatch_block_t)block { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); } /** * Questions? Have you read the header file? **/ - (int)socketFD { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } if (socket4FD != SOCKET_NULL) return socket4FD; else return socket6FD; } /** * Questions? Have you read the header file? **/ - (int)socket4FD { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } return socket4FD; } /** * Questions? Have you read the header file? **/ - (int)socket6FD { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } return socket6FD; } #if TARGET_OS_IPHONE /** * Questions? Have you read the header file? **/ - (CFReadStreamRef)readStream { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } if (readStream == NULL) [self createReadAndWriteStream]; return readStream; } /** * Questions? Have you read the header file? **/ - (CFWriteStreamRef)writeStream { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } if (writeStream == NULL) [self createReadAndWriteStream]; return writeStream; } - (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat { if (![self createReadAndWriteStream]) { // Error occurred creating streams (perhaps socket isn't open) return NO; } BOOL r1, r2; LogVerbose(@"Enabling backgrouding on socket"); r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); if (!r1 || !r2) { return NO; } if (!caveat) { if (![self openStreams]) { return NO; } } return YES; } /** * Questions? Have you read the header file? **/ - (BOOL)enableBackgroundingOnSocket { LogTrace(); if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NO; } return [self enableBackgroundingOnSocketWithCaveat:NO]; } - (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? { // This method was created as a workaround for a bug in iOS. // Apple has since fixed this bug. // I'm not entirely sure which version of iOS they fixed it in... LogTrace(); if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NO; } return [self enableBackgroundingOnSocketWithCaveat:YES]; } #endif - (SSLContextRef)sslContext { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } return sslContext; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Class Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr { LogTrace(); NSMutableArray *addresses = nil; NSError *error = nil; if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) { // Use LOOPBACK address struct sockaddr_in nativeAddr4; nativeAddr4.sin_len = sizeof(struct sockaddr_in); nativeAddr4.sin_family = AF_INET; nativeAddr4.sin_port = htons(port); nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); struct sockaddr_in6 nativeAddr6; nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); nativeAddr6.sin6_family = AF_INET6; nativeAddr6.sin6_port = htons(port); nativeAddr6.sin6_flowinfo = 0; nativeAddr6.sin6_addr = in6addr_loopback; nativeAddr6.sin6_scope_id = 0; // Wrap the native address structures NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; addresses = [NSMutableArray arrayWithCapacity:2]; [addresses addObject:address4]; [addresses addObject:address6]; } else { NSString *portStr = [NSString stringWithFormat:@"%hu", port]; struct addrinfo hints, *res, *res0; memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); if (gai_error) { error = [self gaiError:gai_error]; } else { NSUInteger capacity = 0; for (res = res0; res; res = res->ai_next) { if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { capacity++; } } addresses = [NSMutableArray arrayWithCapacity:capacity]; for (res = res0; res; res = res->ai_next) { if (res->ai_family == AF_INET) { // Found IPv4 address. // Wrap the native address structure, and add to results. NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address4]; } else if (res->ai_family == AF_INET6) { // Fixes connection issues with IPv6 // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 // Found IPv6 address. // Wrap the native address structure, and add to results. struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)res->ai_addr; in_port_t *portPtr = &sockaddr->sin6_port; if ((portPtr != NULL) && (*portPtr == 0)) { *portPtr = htons(port); } NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address6]; } } freeaddrinfo(res0); if ([addresses count] == 0) { error = [self gaiError:EAI_FAIL]; } } } if (errPtr) *errPtr = error; return addresses; } + (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { char addrBuf[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { char addrBuf[INET6_ADDRSTRLEN]; if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { return ntohs(pSockaddr4->sin_port); } + (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { return ntohs(pSockaddr6->sin6_port); } + (NSURL *)urlFromSockaddrUN:(const struct sockaddr_un *)pSockaddr { NSString *path = [NSString stringWithUTF8String:pSockaddr->sun_path]; return [NSURL fileURLWithPath:path]; } + (NSString *)hostFromAddress:(NSData *)address { NSString *host; if ([self getHost:&host port:NULL fromAddress:address]) return host; else return nil; } + (uint16_t)portFromAddress:(NSData *)address { uint16_t port; if ([self getHost:NULL port:&port fromAddress:address]) return port; else return 0; } + (BOOL)isIPv4Address:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; if (sockaddrX->sa_family == AF_INET) { return YES; } } return NO; } + (BOOL)isIPv6Address:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; if (sockaddrX->sa_family == AF_INET6) { return YES; } } return NO; } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address { return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; if (sockaddrX->sa_family == AF_INET) { if ([address length] >= sizeof(struct sockaddr_in)) { struct sockaddr_in sockaddr4; memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; if (afPtr) *afPtr = AF_INET; return YES; } } else if (sockaddrX->sa_family == AF_INET6) { if ([address length] >= sizeof(struct sockaddr_in6)) { struct sockaddr_in6 sockaddr6; memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; if (afPtr) *afPtr = AF_INET6; return YES; } } } return NO; } + (NSData *)CRLFData { return [NSData dataWithBytes:"\x0D\x0A" length:2]; } + (NSData *)CRData { return [NSData dataWithBytes:"\x0D" length:1]; } + (NSData *)LFData { return [NSData dataWithBytes:"\x0A" length:1]; } + (NSData *)ZeroData { return [NSData dataWithBytes:"" length:1]; } #pragma mark - #pragma mark Third Party Modifications - (BOOL)useStrictTimers { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return (config & kUseStrictTimers); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = (config & kUseStrictTimers); }); return result; } } - (void)setUseStrictTimers:(BOOL)flag { dispatch_block_t block = ^{ if (flag) config |= kUseStrictTimers; else config &= ~kUseStrictTimers; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } @end ================================================ FILE: Sources/Shared/Library/External Libraries/Sockets/GCDAsyncSocketExtensions.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "GCDAsyncSocketExtensions.h" NS_ASSUME_NONNULL_BEGIN @implementation GCDAsyncSocket (GCDsyncSocketExtensions) + (instancetype)socketWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq { return [[self alloc] initWithDelegate:aDelegate delegateQueue:dq socketQueue:sq]; } - (tls_protocol_version_t)tlsNegotiatedProtocol { __block SSLProtocol protocol; dispatch_block_t block = ^{ TEXTUAL_IGNORE_DEPRECATION_BEGIN OSStatus status = SSLGetNegotiatedProtocolVersion(self.sslContext, &protocol); TEXTUAL_IGNORE_DEPRECATION_END #pragma unused(status) }; [self performBlock:block]; return [RCMSecureTransport protocolTypeFromDeprecated:protocol]; } - (tls_ciphersuite_t)tlsNegotiatedCipherSuite { __block SSLCipherSuite cipher; dispatch_block_t block = ^{ TEXTUAL_IGNORE_DEPRECATION_BEGIN OSStatus status = SSLGetNegotiatedCipher(self.sslContext, &cipher); TEXTUAL_IGNORE_DEPRECATION_END #pragma unused(status) }; [self performBlock:block]; /* This can be easily cast because they refer to the same code points. */ return (tls_ciphersuite_t)cipher; } - (SecTrustRef)tlsTrustRef { __block SecTrustRef trust; dispatch_block_t block = ^{ TEXTUAL_IGNORE_DEPRECATION_BEGIN OSStatus status = SSLCopyPeerTrust(self.sslContext, &trust); TEXTUAL_IGNORE_DEPRECATION_END #pragma unused(status) }; [self performBlock:block]; return trust; } - (nullable NSArray *)tlsCertificateChainData { SecTrustRef trustRef = self.tlsTrustRef; if (trustRef == NULL) { return nil; } return [RCMSecureTransport certificatesInTrust:trustRef]; } - (nullable NSString *)tlsPolicyName { SecTrustRef trustRef = self.tlsTrustRef; if (trustRef == NULL) { return nil; } return [RCMSecureTransport policyNameInTrust:trustRef]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Library/TLOLocalization.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOLocalization.h" NS_ASSUME_NONNULL_BEGIN /* Extension declared by TLOLocalization.swift */ @interface NSString (LocalizationPrivate) + (NSString *)_swift_localizedKey:(NSString *)string bundle:(NSBundle *)bundle; @end NSString *TXTLS(NSString *key, ...) { NSCParameterAssert(key != nil); va_list arguments; va_start(arguments, key); NSString *result = TXLocalizedString(RZMainBundle(), key, arguments); va_end(arguments); return result; } NSString *TXLocalizedString(NSBundle *bundle, NSString *key, va_list arguments) { NSCParameterAssert(bundle != nil); NSCParameterAssert(key != nil); NSCParameterAssert(arguments != NULL); NSString *localValue = [NSString _swift_localizedKey:key bundle:bundle]; return [[NSString alloc] initWithFormat:localValue arguments:arguments]; } NSString *TXLocalizedStringAlternative(NSBundle *bundle, NSString *key, ...) { NSCParameterAssert(bundle != nil); NSCParameterAssert(key != nil); va_list arguments; va_start(arguments, key); NSString *result = TXLocalizedString(bundle, key, arguments); va_end(arguments); return result; } NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Library/TLOLocalization.swift ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /// /// Given an input string, table, and bundle; these helpers perform localization. /// /// • "BasicLanguage" is the default localization table. /// • The main bundle is the default localization bundle. /// /// When the "specialKey" argument is false, the input string is not treated special. /// It and the other arguments are handed directly to NSLocalizedString(). /// /// Textual has a unique localization system, which is enabled by setting "specialKey" to true. /// When set to true, the input string is expected to be in the format: "[]" /// /// Everything left of the first open bracket ("[") is treated as the table name. /// Everything inside of the first open bracket ("[") and first close bracket ("]") /// is treated as the "key" - The key is typically just a random combination of characters. /// /// For example: "'7r2-4h' = 'Some text'"; in Common.strings is accessed with "Common[7r2-4h]" /// /// When the key is assigned in the strings file, it is not prefaced by the table name. /// /// /// Performs localization with a special key /// public func LocalizedKey(_ key: String, _ arguments: CVarArg..., table: String = "BasicLanguage", bundle: Bundle = Bundle.main) -> String { return localize(string: key, arguments: arguments, table: table, bundle: bundle, specialKey: true) } /// /// Performs localization with input string /// public func LocalizedString(_ string: String, _ arguments: CVarArg..., table: String = "BasicLanguage", bundle: Bundle = Bundle.main) -> String { return localize(string: string, arguments: arguments, table: table, bundle: bundle, specialKey: false) } @inline(__always) fileprivate func localize(string: String, arguments: [CVarArg], table: String, bundle: Bundle, specialKey: Bool) -> String { let formatter = String(localized: string, table: table, bundle: bundle, specialKey: specialKey) if (arguments.isEmpty) { return formatter } return String(format: formatter, arguments: arguments) } fileprivate extension String { init (localized string: String, table: String, bundle: Bundle, specialKey: Bool) { guard specialKey, let openBracket = string.firstIndex(of: "["), let closeBracket = string.firstIndex(of: "]") else { self = bundle.localizedString(forKey: string, value: nil, table: table) return } /* Given keys in the format "
[]", extract the two values and lookup the result. */ let tableName = String(string[string.startIndex ..< openBracket]) let tableKey = String(string[(string.index(openBracket, offsetBy: 1)) ..< closeBracket]) /* Backwards compatibility for plugins */ // // The format of key assignments changed in version 7.1.0. // In prior versions, the table name was included in the assignment. // // Assignment in 7.1.0: // "7r2-4h" = "Some text"; // // Assignment before 7.1.0: // "Common[0001]" = "Some text"; // // To support plugins that still have the old format compiled in, // we check whether the key we have contains a dash. // // Keys prior to version 7.0.10 /should/ not ever contain a dash. // // If a dash is present, then we use the original input string as key. // if (tableKey.contains("-")) { self = bundle.localizedString(forKey: tableKey, value: nil, table: tableName) } else { self = bundle.localizedString(forKey: string, value: nil, table: tableName) } } } // // This extension gives TXTLS() and its sister C functions access // to the logic needed to pluck out a localized string. // Those functions will perform argument formatting on their end // given the result because you can't pass arguments from C -> Swift. // extension NSString { @objc(_swift_localizedKey:bundle:) class func localize(key: String, bundle: Bundle) -> NSString { return String(localized: key, table: "BasicLanguage", bundle: bundle, specialKey: true) as NSString } } ================================================ FILE: Sources/Shared/Library/TLOTimer.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TLOTimer.h" NS_ASSUME_NONNULL_BEGIN @interface TLOTimer () @property (nonatomic, copy, readwrite) TLOTimerActionBlock actionBlock; @property (nonatomic, assign, readwrite) NSTimeInterval startTime; @property (nonatomic, assign, readwrite) NSTimeInterval interval; @property (nonatomic, assign, readwrite) BOOL repeatTimer; @property (nonatomic, assign, readwrite) NSUInteger iterations; @property (nonatomic, assign, readwrite) NSUInteger currentIteration; @property (nonatomic, strong, nullable) dispatch_source_t timerSource; @end @implementation TLOTimer + (instancetype)timerWithActionBlock:(TLOTimerActionBlock)actionBlock { NSParameterAssert(actionBlock != NULL); return [[self alloc] initWithActionBlock:actionBlock onQueue:nil]; } + (instancetype)timerWithActionBlock:(TLOTimerActionBlock)actionBlock onQueue:(dispatch_queue_t)queue { NSParameterAssert(actionBlock != NULL); NSParameterAssert(queue != NULL); return [[self alloc] initWithActionBlock:actionBlock onQueue:queue]; } - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithActionBlock:(TLOTimerActionBlock)actionBlock { return [self initWithActionBlock:actionBlock onQueue:nil]; } - (instancetype)initWithActionBlock:(TLOTimerActionBlock)actionBlock onQueue:(dispatch_queue_t)queue { NSParameterAssert(actionBlock != NULL); if ((self = [super init])) { self.actionBlock = actionBlock; self.queue = queue; return self; } return nil; } - (void)dealloc { [self stop]; } - (BOOL)timerIsActive { return (self.timerSource != nil); } - (NSTimeInterval)timeRemaining { return (self.interval - (CFAbsoluteTimeGetCurrent() - self.startTime)); } - (void)start:(NSTimeInterval)timerInterval { [self start:timerInterval onRepeat:NO iterations:0]; } - (void)start:(NSTimeInterval)timerInterval onRepeat:(BOOL)repeatTimer { [self start:timerInterval onRepeat:repeatTimer iterations:0]; } - (void)start:(NSTimeInterval)timerInterval onRepeat:(BOOL)repeatTimer iterations:(NSUInteger)iterations { NSParameterAssert(timerInterval > 0); [self stop]; dispatch_queue_t sourceQueue = self.queue; if (sourceQueue == nil) { sourceQueue = dispatch_get_main_queue(); } dispatch_source_t timerSource = XRScheduleBlockOnQueue(sourceQueue, ^{ [self fireTimer]; }, timerInterval, repeatTimer); self.interval = timerInterval; self.repeatTimer = repeatTimer; self.iterations = iterations; self.currentIteration = 0; self.timerSource = timerSource; XRResumeScheduledBlock(timerSource); self.startTime = CFAbsoluteTimeGetCurrent(); } - (void)stop { dispatch_source_t timerSource = self.timerSource; if (timerSource == nil) { return; } XRCancelScheduledBlock(timerSource); self.timerSource = nil; } - (void)stopIfNeeded { if (self.iterations > 0 && self.iterations == self.currentIteration) { [self stop]; } } - (void)fireTimer { self.currentIteration += 1; /* In the logic presented below, we call -stopIfNeeded before the action. */ /* We should probably always keep this progression so that the action can know when the iterations limit has been reached by the fact the timer has stopped. */ /* Perform block */ TLOTimerActionBlock actionBlock = self.actionBlock; [self stopIfNeeded]; actionBlock(self); } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Preferences/TPCPreferences.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2008 - 2010 Satoshi Nakagawa * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferencesUserDefaults.h" #import "TPCPreferencesPrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation TPCPreferences #pragma mark - #pragma mark Inline Image Size + (uint64_t)inlineImagesMaxFilesize { NSUInteger filesizeTag = [RZUserDefaults() unsignedIntegerForKey:@"InlineMediaMaximumFilesize"]; switch (filesizeTag) { #define _dv(key, value) case (key): { return (value); } _dv(1, (uint64_t)1048576) // 1 MB _dv(2, (uint64_t)2097152) // 2 MB _dv(3, (uint64_t)3145728) // 3 MB _dv(4, (uint64_t)4194304) // 4 MB _dv(5, (uint64_t)5242880) // 5 MB _dv(6, (uint64_t)10485760) // 10 MB _dv(7, (uint64_t)15728640) // 15 MB _dv(8, (uint64_t)20971520) // 20 MB _dv(9, (uint64_t)52428800) // 50 MB _dv(10, (uint64_t)104857600) // 100 MB #undef _dv } return (uint64_t)2097152; // 2 MB } + (NSUInteger)inlineMediaMaxWidth { return [RZUserDefaults() unsignedIntegerForKey:@"InlineMediaScalingWidth"]; } + (NSUInteger)inlineMediaMaxHeight { return [RZUserDefaults() unsignedIntegerForKey:@"InlineMediaMaximumHeight"]; } + (void)setInlineMediaMaxWidth:(NSUInteger)value { [RZUserDefaults() setUnsignedInteger:value forKey:@"InlineMediaScalingWidth"]; } + (void)setInlineMediaMaxHeight:(NSUInteger)value { [RZUserDefaults() setUnsignedInteger:value forKey:@"InlineMediaMaximumHeight"]; } + (BOOL)inlineMediaLimitToBasics { return [RZUserDefaults() boolForKey:@"InlineMediaLimitToBasics"]; } + (void)setInlineMediaLimitToBasics:(BOOL)inlineMediaLimitToBasics { [RZUserDefaults() setBool:inlineMediaLimitToBasics forKey:@"InlineMediaLimitToBasics"]; } + (BOOL)inlineMediaLimitBasicsToFiles { return [RZUserDefaults() boolForKey:@"InlineMediaLimitBasicsToFiles"]; } + (void)setInlineMediaLimitBasicsToFiles:(BOOL)inlineMediaLimitBasicsToFiles { [RZUserDefaults() setBool:inlineMediaLimitBasicsToFiles forKey:@"InlineMediaLimitBasicsToFiles"]; } + (BOOL)inlineMediaLimitInsecureContent { return [RZUserDefaults() boolForKey:@"InlineMediaLimitInsecureContent"]; } + (BOOL)inlineMediaLimitNaughtyContent { return [RZUserDefaults() boolForKey:@"InlineMediaLimitNaughtyContent"]; } + (BOOL)inlineMediaLimitUnsafeContent { return [RZUserDefaults() boolForKey:@"InlineMediaLimitUnsafeContent"]; } + (BOOL)inlineMediaCheckEverything { return [RZUserDefaults() boolForKey:@"InlineMediaCheckEverything"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Preferences/TPCPreferencesUserDefaults.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #include "BuildConfig.h" #import "TPCPreferencesUserDefaults.h" /* TPCPreferencesUserDefaults is specifically designed for reading and writing from the main app's preferences file, even within an XPC service. */ /* NSUserDefaults can be used in an XPC service if service specific preferences need to be retained somehow. */ NS_ASSUME_NONNULL_BEGIN NSString * const TPCPreferencesUserDefaultsDidChangeNotification = @"TPCPreferencesUserDefaultsDidChangeNotification"; #pragma mark - #pragma mark Reading & Writing @implementation TPCPreferencesUserDefaults + (TPCPreferencesUserDefaults *)sharedUserDefaults { static id sharedSelf = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedSelf = [[self alloc] _initGroupContainer]; }); return sharedSelf; } - (instancetype)_initGroupContainer { TPCPreferencesUserDefaults *defaults = [super initWithSuiteName:TXBundleBuildGroupContainerIdentifier]; return defaults; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-designated-initializers" - (instancetype)init { return [self.class sharedUserDefaults]; } - (nullable instancetype)initWithSuiteName:(nullable NSString *)suitename { return [self.class sharedUserDefaults]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (nullable instancetype)initWithUser:(NSString *)username { return [self.class sharedUserDefaults]; } #pragma clang diagnostic pop #pragma clang diagnostic pop - (void)_setObject:(nullable id)value forKey:(NSString *)defaultName { [super setObject:value forKey:defaultName]; } - (void)setObject:(nullable id)value forKey:(NSString *)defaultName { [self setObject:value forKey:defaultName postNotification:YES]; } - (void)setObject:(nullable id)value forKey:(NSString *)defaultName postNotification:(BOOL)postNotification { NSParameterAssert(defaultName != nil); id oldValue = [self objectForKey:defaultName]; if (oldValue && oldValue == value) { return; } [self willChangeValueForKey:defaultName]; if (value == nil) { if (oldValue) { [self _setObject:nil forKey:defaultName]; } } else { [self _setObject:value forKey:defaultName]; } [self didChangeValueForKey:defaultName]; if (postNotification) { [RZNotificationCenter() postNotificationName:TPCPreferencesUserDefaultsDidChangeNotification object:self userInfo:@{@"changedKey" : defaultName}]; /* We currently don't need to communicate preferences changes between the main app and XPC services, but if we do, then we should enable this code. */ #if 0 [RZDistributedNotificationCenter() postNotificationName:TPCPreferencesUserDefaultsDidChangeNotification object:@"TPCPreferencesUserDefaults" userInfo:@{@"changedKey" : defaultName}]; #endif } } - (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setUnsignedInteger:(NSUInteger)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setShort:(short)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setUnsignedShort:(unsigned short)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setLong:(long)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setUnsignedLong:(unsigned long)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setLongLong:(long long)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setUnsignedLongLong:(unsigned long long)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setFloat:(float)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setDouble:(double)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setBool:(BOOL)value forKey:(NSString *)defaultName { [self setObject:@(value) forKey:defaultName]; } - (void)setURL:(nullable NSURL *)value forKey:(NSString *)defaultName { [self setObject:value forKey:defaultName]; } - (void)removeObjectForKey:(NSString *)defaultName { [self setObject:nil forKey:defaultName]; } - (void)registerDefault:(id )value forKey:(NSString *)defaultName { NSParameterAssert(value != nil); NSParameterAssert(defaultName != nil); [self registerDefaults:@{defaultName : value}]; } - (NSDictionary *)registeredDefaults { return [self volatileDomainForName:NSRegistrationDomain]; } @end #pragma mark - #pragma mark Object KVO Proxying @implementation TPCPreferencesUserDefaultsController - (instancetype)_initWithSharedDefaults { TPCPreferencesUserDefaults *defaults = [TPCPreferencesUserDefaults sharedUserDefaults]; return [super initWithDefaults:defaults initialValues:nil]; } - (instancetype)init { return [self _initWithSharedDefaults]; } - (nullable instancetype)initWithCoder:(NSCoder *)coder { return [self _initWithSharedDefaults]; } - (instancetype)initWithDefaults:(nullable NSUserDefaults *)defaults initialValues:(nullable NSDictionary *)initialValues { return [self _initWithSharedDefaults]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sources/Shared/Views/Channel View/TVCLogLineXPC.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TVCLogLineXPCPrivate.h" NS_ASSUME_NONNULL_BEGIN /* TVCLogLineXPC is a container class for TVCLogLine when stored in a Core Data store. -data is the secure coded version of the class which is portable and can be stored in an offline database. */ @interface TVCLogLineXPC () @property (nonatomic, copy, readwrite) NSData *data; @property (nonatomic, copy, readwrite) NSString *uniqueIdentifier; @property (nonatomic, copy, readwrite) NSString *viewIdentifier; @property (nonatomic, assign, readwrite) NSUInteger sessionIdentifier; @property (nonatomic, assign, readwrite) NSTimeInterval creationDate; @end @implementation TVCLogLineXPC - (instancetype)initWithLogLineData:(NSData *)data uniqueIdentifier:(NSString *)uniqueIdentifier viewIdentifier:(NSString *)viewIdentifier sessionIdentifier:(NSUInteger)sessionIdentifier { NSParameterAssert(data != nil); NSParameterAssert(uniqueIdentifier != nil); NSParameterAssert(viewIdentifier != nil); if ((self = [super init])) { self.data = data; self.uniqueIdentifier = uniqueIdentifier; self.viewIdentifier = viewIdentifier; self.sessionIdentifier = sessionIdentifier; return self; } return nil; } - (instancetype)initWithManagedObject:(NSManagedObject *)managedObject { NSParameterAssert(managedObject != nil); if ((self = [super init])) { self.data = [managedObject valueForKey:@"logLineData"]; self.uniqueIdentifier = [managedObject valueForKey:@"logLineUniqueIdentifier"]; self.viewIdentifier = [managedObject valueForKey:@"logLineViewIdentifier"]; self.sessionIdentifier = [[managedObject valueForKey:@"sessionIdentifier"] integerValue]; self.creationDate = [[managedObject valueForKey:@"entryCreationDate"] doubleValue]; return self; } return nil; } - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { NSParameterAssert(aDecoder != nil); if ((self = [super init])) { [self decodeWithCoder:aDecoder]; return self; } return nil; } - (void)decodeWithCoder:(NSCoder *)aDecoder { NSParameterAssert(aDecoder != nil); self->_data = [aDecoder decodeDataForKey:@"data"]; self->_uniqueIdentifier = [aDecoder decodeStringForKey:@"uniqueIdentifier"]; self->_viewIdentifier = [aDecoder decodeStringForKey:@"viewIdentifier"]; self->_sessionIdentifier = [aDecoder decodeIntegerForKey:@"sessionIdentifier"]; self->_creationDate = [aDecoder decodeDoubleForKey:@"entryCreationDate"]; } - (void)encodeWithCoder:(NSCoder *)aCoder { NSParameterAssert(aCoder != nil); [aCoder encodeData:self.data forKey:@"data"]; [aCoder encodeObject:self.uniqueIdentifier forKey:@"uniqueIdentifier"]; [aCoder encodeObject:self.viewIdentifier forKey:@"viewIdentifier"]; [aCoder encodeInteger:self.sessionIdentifier forKey:@"sessionIdentifier"]; [aCoder encodeDouble:self.creationDate forKey:@"entryCreationDate"]; } + (BOOL)supportsSecureCoding { return YES; } - (NSString *)description { return [NSString stringWithFormat:@"", self.uniqueIdentifier, self.creationDate]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/HLSHistoricLogLineEntityMigration.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @implementation HLSHistoricLogLineEntityMigration - (NSString *)newLogLineUniqueIdentifier { NSString *printIdentifier = [NSString stringWithUUID]; // Example: 68753A44-4D6F-1226-9C60-0050E4C00067 return [printIdentifier substringFromIndex:19]; // Example: 9C60-0050E4C00067 } - (NSNumber *)newSessionIdentifier { static NSUInteger sessionIdentifier = 0; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sessionIdentifier = arc4random_uniform(999999); }); return @(sessionIdentifier); } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/HLSHistoricLogProcessMain.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferencesUserDefaults.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, HLSHistoricLogUniqueIdentifierFetchType) { HLSHistoricLogReturnEntriesUniqueIdentifierTypeBefore, HLSHistoricLogReturnEntriesUniqueIdentifierTypeAfter }; @interface HLSHistoricLogProcessMain () @property (nonatomic, strong) NSXPCConnection *serviceConnection; @property (nonatomic, assign) BOOL isPerformingSave; @property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; @property (nonatomic, strong) NSManagedObjectModel *managedObjectModel; @property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator; @property (nonatomic, copy) NSString *databasePath; // Path to database file @property (nonatomic, copy) NSString *databaseDirectory; // Path to database directory /* contextObjects is mutable. It should only be accessed in a queue. Use the global context's queue. */ @property (nonatomic, strong) NSMutableDictionary *contextObjects; @property (nonatomic, assign) NSUInteger maximumLineCount; @property (nonatomic, strong) dispatch_source_t saveTimer; @end @implementation HLSHistoricLogProcessMain - (instancetype)initWithConnection:(NSXPCConnection *)connection { NSParameterAssert(connection != nil); if ((self = [super init])) { self.serviceConnection = connection; LogToConsoleSetDefaultSubsystemToMainBundle(@"General"); [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.contextObjects = [NSMutableDictionary dictionary]; self.maximumLineCount = 100; } - (void)_resetDatabaseFilename { NSString *filename = [NSString stringWithFormat:@"logControllerHistoricLog_%@.sqlite", [NSString stringWithUUID]]; [RZUserDefaults() setObject:filename forKey:@"TVCLogControllerHistoricLogFileSavePath_v3"]; } - (NSString *)_databaseSaveFilename { NSString *filename = [RZUserDefaults() objectForKey:@"TVCLogControllerHistoricLogFileSavePath_v3"]; if (filename == nil) { [self _resetDatabaseFilename]; } return filename; } - (void)_setDatabasePathInDirectory:(NSString *)databaseDirectory { NSParameterAssert(databaseDirectory != nil); self.databaseDirectory = databaseDirectory; [self _setDatabasePath]; } - (void)_setDatabasePath { NSString *filename = [self _databaseSaveFilename]; NSString *databasePath = [self.databaseDirectory stringByAppendingPathComponent:filename]; self.databasePath = databasePath; } - (void)_resetDatabasePath { [self _resetDatabaseFilename]; [self _setDatabasePath]; } - (void)openDatabaseInDirectory:(NSString *)databaseDirectory withCompletionBlock:(void (NS_NOESCAPE ^ _Nullable)(BOOL))completionBlock { NSParameterAssert(databaseDirectory != nil); [self _setDatabasePathInDirectory:databaseDirectory]; LogToConsoleInfo("Opening database at path: %{public}@", self.databasePath.standardizedTildePath); BOOL success = [self _createBaseModel]; if (completionBlock) { completionBlock(success); } if (success == NO) { return; } [self _rescheduleSave]; } - (void)setMaximumLineCount:(NSUInteger)maximumLineCount { NSParameterAssert(maximumLineCount > 0); if (self->_maximumLineCount != maximumLineCount) { self->_maximumLineCount = maximumLineCount; } } - (NSFetchRequest *)_fetchRequestForView:(NSString *)viewId fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate resultType:(NSFetchRequestResultType)resultType { return [self _fetchRequestForView:viewId ascending:YES fetchLimit:fetchLimit lowestEntryIdentifier:0 highestEntryIdentifier:NSIntegerMax limitToDate:limitToDate resultType:resultType]; } - (NSFetchRequest *)_fetchRequestForView:(NSString *)viewId ascending:(BOOL)ascending fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate resultType:(NSFetchRequestResultType)resultType { return [self _fetchRequestForView:viewId ascending:ascending fetchLimit:fetchLimit lowestEntryIdentifier:0 highestEntryIdentifier:NSIntegerMax limitToDate:limitToDate resultType:resultType]; } - (NSFetchRequest *)_fetchRequestForView:(NSString *)viewId ascending:(BOOL)ascending fetchLimit:(NSUInteger)fetchLimit lowestEntryIdentifier:(NSUInteger)lowestEntryIdentifier highestEntryIdentifier:(NSUInteger)highestEntryIdentifier limitToDate:(nullable NSDate *)limitToDate resultType:(NSFetchRequestResultType)resultType { NSParameterAssert(viewId != nil); if (limitToDate == nil) { limitToDate = [NSDate distantFuture]; } NSDictionary *substitutionVariables = @{ @"view_id" : viewId, @"entry_id_lowest" : @(lowestEntryIdentifier), @"entry_id_highest" : @(highestEntryIdentifier), @"creation_date" : @([limitToDate timeIntervalSince1970]) }; NSFetchRequest *fetchRequest = [self.managedObjectModel fetchRequestFromTemplateWithName:@"GenericConditional" substitutionVariables:substitutionVariables]; if (fetchLimit > 0) { fetchRequest.fetchLimit = fetchLimit; } fetchRequest.includesPendingChanges = YES; fetchRequest.includesPropertyValues = YES; fetchRequest.returnsObjectsAsFaults = NO; fetchRequest.resultType = resultType; fetchRequest.sortDescriptors = @[[[NSSortDescriptor alloc] initWithKey:@"entryCreationDate" ascending:ascending]]; return fetchRequest; } - (void)forgetView:(NSString *)viewId { NSParameterAssert(viewId != nil); LogToConsoleDebug("Forgetting view: %{public}@", viewId); HLSHistoricLogViewContext *viewContext = [self contextForView:viewId]; [viewContext performBlockAndWait:^{ [self cancelResizeInViewContext:viewContext]; NSFetchRequest *fetchRequest = [self _fetchRequestForView:viewContext.hls_viewId fetchLimit:0 limitToDate:nil resultType:NSManagedObjectResultType]; [self _deleteDataInViewContext:viewContext withFetchRequest:fetchRequest performOnQueue:NO]; [viewContext reset]; }]; NSManagedObjectContext *parentContext = self.managedObjectContext; [parentContext performBlockAndWait:^{ [self.contextObjects removeObjectForKey:viewId]; }]; } - (void)resetDataForView:(NSString *)viewId { NSParameterAssert(viewId != nil); LogToConsoleDebug("Resetting the contents of view: %{public}@", viewId); HLSHistoricLogViewContext *viewContext = [self contextForView:viewId]; [viewContext performBlockAndWait:^{ [self cancelResizeInViewContext:viewContext]; NSFetchRequest *fetchRequest = [self _fetchRequestForView:viewContext.hls_viewId fetchLimit:0 limitToDate:nil resultType:NSManagedObjectResultType]; [self _deleteDataInViewContext:viewContext withFetchRequest:fetchRequest performOnQueue:NO]; [viewContext reset]; }]; } - (void)fetchEntriesForView:(NSString *)viewId beforeUniqueIdentifier:(NSString *)uniqueId fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock { return [self fetchEntriesForView:viewId withUniqueIdentifier:uniqueId fetchType:HLSHistoricLogReturnEntriesUniqueIdentifierTypeBefore fetchLimit:fetchLimit limitToDate:limitToDate withCompletionBlock:completionBlock]; } - (void)fetchEntriesForView:(NSString *)viewId afterUniqueIdentifier:(NSString *)uniqueId fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock { return [self fetchEntriesForView:viewId withUniqueIdentifier:uniqueId fetchType:HLSHistoricLogReturnEntriesUniqueIdentifierTypeAfter fetchLimit:fetchLimit limitToDate:limitToDate withCompletionBlock:completionBlock]; } /* This method is used to get line matching unique identifier and any that surround it. */ - (void)fetchEntriesForView:(NSString *)viewId withUniqueIdentifier:(NSString *)uniqueId beforeFetchLimit:(NSUInteger)fetchLimitBefore afterFetchLimit:(NSUInteger)fetchLimitAfter limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock { NSParameterAssert(viewId != nil); NSParameterAssert(uniqueId != nil); HLSHistoricLogViewContext *viewContext = [self contextForView:viewId]; [viewContext performBlockAndWait:^{ NSUInteger firstEntryId = [self _identifierInViewContext:viewContext forUniqueIdentifier:uniqueId performOnQueue:NO]; if (firstEntryId == NSNotFound) { completionBlock(@[]); return; } NSInteger lowestEntryId = (firstEntryId - fetchLimitBefore); NSInteger highestEntryId = (firstEntryId + fetchLimitAfter); NSFetchRequest *fetchRequest = [self _fetchRequestForView:viewContext.hls_viewId ascending:YES fetchLimit:0 lowestEntryIdentifier:lowestEntryId highestEntryIdentifier:highestEntryId limitToDate:limitToDate resultType:NSManagedObjectResultType]; NSError *fetchRequestError = nil; NSArray *fetchedObjects = [viewContext executeFetchRequest:fetchRequest error:&fetchRequestError]; if (fetchedObjects == nil) { LogToConsoleError("Error occurred fetching objects: %{public}@", fetchRequestError.localizedDescription); return; } LogToConsoleDebug("%{public}lu results fetched for view %{public}@", fetchedObjects.count, viewId); @autoreleasepool { NSArray *fetchedEntries = [self _logLineXPCObjectsFromManagedObjects:fetchedObjects]; completionBlock([fetchedEntries copy]); } }]; } /* This method is used to get a list of lines between two unique identifiers. */ - (void)fetchEntriesForView:(NSString *)viewId afterUniqueIdentifier:(NSString *)uniqueIdAfter beforeUniqueIdentifier:(NSString *)uniqueIdBefore fetchLimit:(NSUInteger)fetchLimit withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock { NSParameterAssert(viewId != nil); NSParameterAssert(uniqueIdAfter != nil); NSParameterAssert(uniqueIdBefore != nil); HLSHistoricLogViewContext *viewContext = [self contextForView:viewId]; [viewContext performBlockAndWait:^{ NSUInteger firstEntryId = [self _identifierInViewContext:viewContext forUniqueIdentifier:uniqueIdAfter performOnQueue:NO]; NSUInteger secondEntryId = [self _identifierInViewContext:viewContext forUniqueIdentifier:uniqueIdBefore performOnQueue:NO]; if (firstEntryId == NSNotFound || secondEntryId == NSNotFound) { completionBlock(@[]); return; } /* We are getting the lines in-between these two lines which means we subtract self. */ NSInteger lowestEntryId = (firstEntryId + 1); NSInteger highestEntryId = (secondEntryId - 1); NSFetchRequest *fetchRequest = [self _fetchRequestForView:viewContext.hls_viewId ascending:YES fetchLimit:fetchLimit lowestEntryIdentifier:lowestEntryId highestEntryIdentifier:highestEntryId limitToDate:nil resultType:NSManagedObjectResultType]; NSError *fetchRequestError = nil; NSArray *fetchedObjects = [viewContext executeFetchRequest:fetchRequest error:&fetchRequestError]; if (fetchedObjects == nil) { LogToConsoleError("Error occurred fetching objects: %{public}@", fetchRequestError.localizedDescription); return; } LogToConsoleDebug("%{public}lu results fetched for view %{public}@", fetchedObjects.count, viewId); @autoreleasepool { NSArray *fetchedEntries = [self _logLineXPCObjectsFromManagedObjects:fetchedObjects]; completionBlock([fetchedEntries copy]); } }]; } - (void)fetchEntriesForView:(NSString *)viewId withUniqueIdentifier:(NSString *)uniqueId fetchType:(HLSHistoricLogUniqueIdentifierFetchType)fetchType fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock { NSParameterAssert(viewId != nil); NSParameterAssert(uniqueId != nil); NSParameterAssert(completionBlock != nil); NSParameterAssert(fetchLimit > 0); HLSHistoricLogViewContext *viewContext = [self contextForView:viewId]; [viewContext performBlockAndWait:^{ /* Unique identifiers are strings. We find what is the the entry identifier for this string. The entry identifier is an integer. We can then subtract or add the fetch limit to that to get the entries we are interested in. */ NSUInteger firstEntryId = [self _identifierInViewContext:viewContext forUniqueIdentifier:uniqueId performOnQueue:NO]; if (firstEntryId == NSNotFound) { completionBlock(@[]); return; } NSInteger lowestEntryId = 0; NSInteger highestEntryId = 0; switch (fetchType) { case HLSHistoricLogReturnEntriesUniqueIdentifierTypeBefore: { /* 1 is subtracted so we can still return fetchLimit while accounting for the fact that firstEntryId is not a value we are interested in. */ lowestEntryId = (firstEntryId - fetchLimit); highestEntryId = (firstEntryId - 1); break; } case HLSHistoricLogReturnEntriesUniqueIdentifierTypeAfter: { lowestEntryId = (firstEntryId + 1); highestEntryId = (firstEntryId + fetchLimit); break; } default: { NSAssert(NO, @"Bad 'fetchType' value"); break; } } NSFetchRequest *fetchRequest = [self _fetchRequestForView:viewContext.hls_viewId ascending:YES fetchLimit:fetchLimit lowestEntryIdentifier:lowestEntryId highestEntryIdentifier:highestEntryId limitToDate:limitToDate resultType:NSManagedObjectResultType]; NSError *fetchRequestError = nil; NSArray *fetchedObjects = [viewContext executeFetchRequest:fetchRequest error:&fetchRequestError]; if (fetchedObjects == nil) { LogToConsoleError("Error occurred fetching objects: %{public}@", fetchRequestError.localizedDescription); return; } LogToConsoleDebug("%{public}lu results fetched for view %{public}@", fetchedObjects.count, viewId); @autoreleasepool { NSArray *fetchedEntries = [self _logLineXPCObjectsFromManagedObjects:fetchedObjects]; completionBlock([fetchedEntries copy]); } }]; } - (void)fetchEntriesForView:(NSString *)viewId ascending:(BOOL)ascending fetchLimit:(NSUInteger)fetchLimit limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock { NSParameterAssert(viewId != nil); NSParameterAssert(completionBlock != nil); HLSHistoricLogViewContext *viewContext = [self contextForView:viewId]; [viewContext performBlockAndWait:^{ NSFetchRequest *fetchRequest = [self _fetchRequestForView:viewContext.hls_viewId ascending:ascending fetchLimit:fetchLimit limitToDate:limitToDate resultType:NSManagedObjectResultType]; NSError *fetchRequestError = nil; NSArray *fetchedObjects = [viewContext executeFetchRequest:fetchRequest error:&fetchRequestError]; if (fetchedObjects == nil) { LogToConsoleError("Error occurred fetching objects: %{public}@", fetchRequestError.localizedDescription); return; } LogToConsoleDebug("%{public}lu results fetched for view %{public}@", fetchedObjects.count, viewId); @autoreleasepool { NSArray *fetchedEntries = [self _logLineXPCObjectsFromManagedObjects:fetchedObjects]; completionBlock([fetchedEntries copy]); } }]; } - (NSArray *)_logLineXPCObjectsFromManagedObjects:(NSArray *)managedObjects { NSParameterAssert(managedObjects != nil); NSMutableArray *xpcObjects = [NSMutableArray arrayWithCapacity:managedObjects.count]; for (NSManagedObject *managedObject in managedObjects) { TVCLogLineXPC *xpcObject = [[TVCLogLineXPC alloc] initWithManagedObject:managedObject]; [xpcObjects addObject:xpcObject]; } return [xpcObjects copy]; } - (void)writeLogLine:(TVCLogLineXPC *)logLine { NSParameterAssert(logLine != nil); HLSHistoricLogViewContext *viewContext = [self contextForView:logLine.viewIdentifier]; [viewContext performBlockAndWait:^{ NSEntityDescription *entity = [NSEntityDescription entityForName:@"LogLine2" inManagedObjectContext:viewContext]; NSManagedObject *newEntry = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:viewContext]; NSUInteger newestIdentifier = [self _incrementNewestIdentifierInViewContext:viewContext]; [newEntry setValue:@(newestIdentifier) forKey:@"entryIdentifier"]; [newEntry setValue:@([[NSDate date] timeIntervalSince1970]) forKey:@"entryCreationDate"]; [newEntry setValue:logLine.viewIdentifier forKey:@"logLineViewIdentifier"]; [newEntry setValue:logLine.data forKey:@"logLineData"]; [newEntry setValue:logLine.uniqueIdentifier forKey:@"logLineUniqueIdentifier"]; [newEntry setValue:@(logLine.sessionIdentifier) forKey:@"sessionIdentifier"]; [self scheduleResizeInViewContext:viewContext]; }]; } - (BOOL)_createBaseModel { return [self _createBaseModelWithRecursion:0]; } - (BOOL)_createBaseModelWithRecursion:(NSUInteger)recursionDepth { NSURL *modelPath = [[NSBundle mainBundle] URLForResource:@"HistoricLogFileStorageModel" withExtension:@"momd"]; NSManagedObjectModel *managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelPath]; NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel]; NSDictionary *pragmaOptions = @{ @"synchronous" : @"NORMAL", @"journal_mode" : @"WAL" }; NSDictionary *persistentStoreOptions = @{ NSMigratePersistentStoresAutomaticallyOption : @(YES), NSInferMappingModelAutomaticallyOption : @(YES), NSSQLitePragmasOption : pragmaOptions }; NSURL *persistentStorePath = [NSURL fileURLWithPath:self.databasePath]; NSError *addPersistentStoreError = nil; NSPersistentStore *persistentStore = [persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:persistentStorePath options:persistentStoreOptions error:&addPersistentStoreError]; if (persistentStore == nil) { LogToConsoleError("Error Creating Persistent Store: %{public}@", addPersistentStoreError.localizedDescription); if (recursionDepth == 0) { LogToConsoleInfo("Attempting to create a new persistent store"); /* If we failed to load our store, we create a brand new one at a new path incase the old one is corrupted. */ [self _resetDatabasePath]; // Destroy any data that may exist return [self _createBaseModelWithRecursion:1]; } return NO; } else { NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator; managedObjectContext.retainsRegisteredObjects = YES; managedObjectContext.undoManager = nil; self.managedObjectContext = managedObjectContext; self.managedObjectModel = managedObjectModel; self.persistentStoreCoordinator = persistentStoreCoordinator; return YES; } } - (void)_rescheduleSave { if (self.saveTimer) { XRCancelScheduledBlock(self.saveTimer); } static NSTimeInterval saveTimerInterval = (60 * 2); // 2 minutes dispatch_source_t saveTimer = XRScheduleBlockOnQueue(dispatch_get_main_queue(), ^{ [self saveDataWithCompletionBlock:nil]; }, saveTimerInterval, YES); XRResumeScheduledBlock(saveTimer); self.saveTimer = saveTimer; } - (void)_quickSaveContext:(NSManagedObjectContext *)context { NSParameterAssert(context != nil); if ([context hasChanges] == NO) { return; } NSError *saveError = nil; if ([context save:&saveError] == NO) { LogToConsoleError("Failed to perform save: %{public}@", saveError.localizedDescription); } [context reset]; } - (void)saveDataWithCompletionBlock:(void (NS_NOESCAPE ^ _Nullable)(void))completionBlock { if (self.isPerformingSave == NO) { self.isPerformingSave = YES; } else { return; } NSManagedObjectContext *context = self.managedObjectContext; [context performBlock:^{ LogToConsoleDebug("Performing save"); [self _rescheduleSave]; [self.contextObjects enumerateKeysAndObjectsUsingBlock:^(NSString *viewId, HLSHistoricLogViewContext *viewContext, BOOL *stop) { [context performBlockAndWait:^{ [self _quickSaveContext:viewContext]; }]; }]; [self _quickSaveContext:context]; self.isPerformingSave = NO; if (completionBlock) { completionBlock(); } }]; } #pragma mark - #pragma mark View Resize Logic - (void)cancelResizeInViewContext:(HLSHistoricLogViewContext *)viewContext { NSParameterAssert(viewContext != nil); if (viewContext.hls_resizeTimer == nil) { return; } XRCancelScheduledBlock(viewContext.hls_resizeTimer); viewContext.hls_resizeTimer = nil; } - (void)scheduleResizeInViewContext:(HLSHistoricLogViewContext *)viewContext { NSParameterAssert(viewContext != nil); if (viewContext.hls_resizeTimer != nil) { return; } if (viewContext.hls_totalLineCount < self.maximumLineCount) { return; } NSString *viewId = viewContext.hls_viewId; NSTimeInterval resizeTimerInterval = (NSTimeInterval)arc4random_uniform(60 * 30); // Somewhere in 30 minutes dispatch_source_t resizeTimer = XRScheduleBlockOnQueue(dispatch_get_main_queue(), ^{ [self resizeView:viewId]; }, resizeTimerInterval, NO); XRResumeScheduledBlock(resizeTimer); viewContext.hls_resizeTimer = resizeTimer; LogToConsoleDebug("Scheduled to resize %{public}@ in %{public}f seconds", viewId, resizeTimerInterval); } - (void)resizeView:(NSString *)viewId { NSParameterAssert(viewId != nil); HLSHistoricLogViewContext *viewContext = [self contextForView:viewId]; [viewContext performBlock:^{ [self _resizeViewContext:viewContext]; }]; } - (void)_resizeViewContext:(HLSHistoricLogViewContext *)viewContext { NSParameterAssert(viewContext != nil); LogToConsoleDebug("Resizing view %{public}@", viewContext.hls_viewId); viewContext.hls_resizeTimer = nil; NSString *viewId = viewContext.hls_viewId; NSInteger lowestIdentifier = (viewContext.hls_newestIdentifier - self.maximumLineCount); NSDictionary *substitutionVariables = @{ @"view_id" : viewId, @"entry_id_lowest" : @(lowestIdentifier) }; NSFetchRequest *fetchRequest = [self.managedObjectModel fetchRequestFromTemplateWithName:@"Truncate" substitutionVariables:substitutionVariables]; fetchRequest.includesPendingChanges = YES; fetchRequest.includesPropertyValues = YES; fetchRequest.returnsObjectsAsFaults = NO; NSUInteger rowsDeleted = [self _deleteDataInViewContext:viewContext withFetchRequest:fetchRequest performOnQueue:NO]; viewContext.hls_totalLineCount -= rowsDeleted; } #pragma mark - #pragma mark Batch Delete Logic - (NSUInteger)_deleteDataInViewContext:(HLSHistoricLogViewContext *)viewContext withFetchRequest:(NSFetchRequest *)fetchRequest performOnQueue:(BOOL)performOnQueue { NSParameterAssert(viewContext != nil); NSParameterAssert(fetchRequest != nil); __block NSUInteger rowsDeleted = 0; dispatch_block_t blockToPerform = ^{ /* Batch delete is not used at the time of this commit because we want the value of a specific property from each managed object before deleting, which old school delete allows us to obtain at the same time we perform delete. */ // if (XRRunningOnOSXElCapitanOrLater()) { // rowsDeleted = [self __deleteDataForFetchRequestUsingBatch:fetchRequest inViewContext:viewContext]; // } else { rowsDeleted = [self __deleteDataForFetchRequestUsingEnumeration:fetchRequest inViewContext:viewContext]; // } }; if (performOnQueue) { [viewContext performBlockAndWait:blockToPerform]; } else { blockToPerform(); } LogToConsoleDebug("Deleted %{public}lu rows in %{public}@", rowsDeleted, viewContext.hls_viewId); return rowsDeleted; } /* - (NSUInteger)__deleteDataForFetchRequestUsingBatch:(NSFetchRequest *)fetchRequest inViewContext:(HLSHistoricLogViewContext *)viewContext { NSParameterAssert(fetchRequest != nil); NSParameterAssert(viewContext != nil); NSBatchDeleteRequest *batchDeleteRequest = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fetchRequest]; batchDeleteRequest.resultType = NSBatchDeleteResultTypeObjectIDs; NSError *batchDeleteError = nil; NSBatchDeleteResult *batchDeleteResult = [viewContext executeRequest:batchDeleteRequest error:&batchDeleteError]; if (batchDeleteResult == nil) { LogToConsoleError("Failed to perform batch delete: %{public}@", batchDeleteError.localizedDescription); return 0; } NSArray *rowsDeleted = batchDeleteResult.result; NSUInteger rowsDeletedCount = rowsDeleted.count; if (rowsDeletedCount > 0) { [NSManagedObjectContext mergeChangesFromRemoteContextSave:@{NSDeletedObjectsKey : rowsDeleted} intoContexts:@[viewContext]]; [self _quickSaveContext:viewContext]; } return rowsDeletedCount; } */ - (NSUInteger)__deleteDataForFetchRequestUsingEnumeration:(NSFetchRequest *)fetchRequest inViewContext:(HLSHistoricLogViewContext *)viewContext { NSParameterAssert(fetchRequest != nil); NSParameterAssert(viewContext != nil); NSError *fetchRequestError = nil; NSArray *fetchedObjects = [viewContext executeFetchRequest:fetchRequest error:&fetchRequestError]; if (fetchedObjects == nil) { LogToConsoleError("Error occurred fetching objects: %{public}@", fetchRequestError.localizedDescription); return 0; } if (fetchedObjects.count == 0) { return 0; } NSMutableArray *uniqueIdentifiers = [NSMutableArray arrayWithCapacity:fetchedObjects.count]; for (NSManagedObject *object in fetchedObjects) { /* Record unique identifier */ NSString *uniqueIdentifier = [object valueForKey:@"logLineUniqueIdentifier"]; if (uniqueIdentifier) { [uniqueIdentifiers addObject:uniqueIdentifier]; } /* Delete object */ [viewContext deleteObject:object]; } [self _quickSaveContext:viewContext]; [self __notifyClientOfDeletedUniqueIdentifiers:[uniqueIdentifiers copy] inViewContext:viewContext]; return fetchedObjects.count; } /* Notify XPC client of intent to delete these unique identifiers. */ /* Deletes can happen based on a timer, without the client asking for it, which means we need a way to inform it of the delete. */ - (void)__notifyClientOfDeletedUniqueIdentifiers:(NSArray *)uniqueIdentifiers inViewContext:(HLSHistoricLogViewContext *)viewContext { NSParameterAssert(uniqueIdentifiers != nil); NSParameterAssert(viewContext != nil); [[self remoteObjectProxy] willDeleteUniqueIdentifiers:uniqueIdentifiers inView:viewContext.hls_viewId]; } #pragma mark - #pragma mark Identifier Cache Management - (HLSHistoricLogViewContext *)contextForView:(NSString *)viewId { NSParameterAssert(viewId != nil); @synchronized(self.contextObjects) { /* Returned cached object or create new */ HLSHistoricLogViewContext *viewContext = self.contextObjects[viewId]; if (viewContext != nil) { return viewContext; } NSManagedObjectContext *parentObjectContext = self.managedObjectContext; viewContext = [[HLSHistoricLogViewContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; /* Properties specific to NSManagedObjectContext */ viewContext.parentContext = parentObjectContext; viewContext.retainsRegisteredObjects = YES; viewContext.undoManager = nil; /* Properties specific to HLSHistoricLogViewContext */ viewContext.hls_viewId = viewId; viewContext.hls_totalLineCount = [self _lineCountInViewContextFromDatabase:viewContext performOnQueue:YES]; viewContext.hls_newestIdentifier = [self _newestIdentifierInViewContextFromDatabase:viewContext performOnQueue:YES]; /* Log information for debugging */ LogToConsoleDebug("Context created for %{public}@ - Line count: %{public}lu, Newest identifier: %{public}lu", viewContext.hls_viewId, viewContext.hls_totalLineCount, viewContext.hls_newestIdentifier); /* Cache new object and return it */ [parentObjectContext performBlockAndWait:^{ self.contextObjects[viewId] = viewContext; }]; return viewContext; } } - (NSUInteger)_incrementNewestIdentifierInViewContext:(HLSHistoricLogViewContext *)viewContext { NSParameterAssert(viewContext != nil); viewContext.hls_totalLineCount += 1; viewContext.hls_newestIdentifier += 1; return viewContext.hls_newestIdentifier; } - (NSUInteger)_newestIdentifierInViewContext:(HLSHistoricLogViewContext *)viewContext { NSParameterAssert(viewContext != nil); return viewContext.hls_newestIdentifier; } - (NSUInteger)_newestIdentifierInViewContextFromDatabase:(HLSHistoricLogViewContext *)viewContext performOnQueue:(BOOL)performOnQueue { NSParameterAssert(viewContext != nil); __block NSUInteger newestIdentifier = 0; dispatch_block_t blockToPerform = ^{ NSFetchRequest *fetchRequest = [self _fetchRequestForView:viewContext.hls_viewId ascending:NO fetchLimit:1 limitToDate:nil resultType:NSManagedObjectResultType]; NSError *fetchRequestError = nil; NSArray *fetchedObjects = [viewContext executeFetchRequest:fetchRequest error:&fetchRequestError]; if (fetchedObjects == nil) { NSAssert1(NO, @"Error occurred fetching objects: %@", fetchRequestError.localizedDescription); } NSManagedObject *fetchedObject = fetchedObjects.firstObject; if (fetchedObject == nil) { return; } NSNumber *newestIdentifierObject = [fetchedObject valueForKey:@"entryIdentifier"]; newestIdentifier = newestIdentifierObject.unsignedIntegerValue; }; if (performOnQueue) { [viewContext performBlockAndWait:blockToPerform]; } else { blockToPerform(); } return newestIdentifier; } - (NSUInteger)_lineCountInViewContextFromDatabase:(HLSHistoricLogViewContext *)viewContext performOnQueue:(BOOL)performOnQueue { NSParameterAssert(viewContext != nil); __block NSUInteger lineCount = 0; dispatch_block_t blockToPerform = ^{ NSFetchRequest *fetchRequest = [self _fetchRequestForView:viewContext.hls_viewId fetchLimit:0 limitToDate:nil resultType:NSCountResultType]; NSError *fetchRequestError = nil; lineCount = [viewContext countForFetchRequest:fetchRequest error:&fetchRequestError]; if (lineCount == NSNotFound) { NSAssert1(NO, @"Error occurred fetching objects: %@", fetchRequestError.localizedDescription); } }; if (performOnQueue) { [viewContext performBlockAndWait:blockToPerform]; } else { blockToPerform(); } return lineCount; } /* Given a logLineUniqueIdentifier, figure out which entryIdentifier is associated with it. */ - (NSUInteger)_identifierInViewContext:(HLSHistoricLogViewContext *)viewContext forUniqueIdentifier:(NSString *)uniqueIdentifier performOnQueue:(BOOL)performOnQueue { NSUInteger identifier = [self _identifierInViewContextFromDatabase:viewContext forUniqueIdentifier:uniqueIdentifier performOnQueue:performOnQueue]; return identifier; } - (NSUInteger)_identifierInViewContextFromDatabase:(HLSHistoricLogViewContext *)viewContext forUniqueIdentifier:(NSString *)uniqueIdentifier performOnQueue:(BOOL)performOnQueue { NSParameterAssert(viewContext != nil); NSParameterAssert(uniqueIdentifier != nil); __block NSUInteger identifier = NSNotFound; dispatch_block_t blockToPerform = ^{ NSString *viewId = viewContext.hls_viewId; NSDictionary *substitutionVariables = @{ @"view_id" : viewId, @"unique_id" : uniqueIdentifier }; NSFetchRequest *fetchRequest = [self.managedObjectModel fetchRequestFromTemplateWithName:@"UniqueIdToEntryId" substitutionVariables:substitutionVariables]; fetchRequest.includesPendingChanges = YES; fetchRequest.includesPropertyValues = YES; fetchRequest.returnsObjectsAsFaults = NO; NSError *fetchRequestError = nil; NSArray *fetchedObjects = [viewContext executeFetchRequest:fetchRequest error:&fetchRequestError]; if (fetchedObjects == nil) { NSAssert1(NO, @"Error occurred fetching objects: %@", fetchRequestError.localizedDescription); } NSManagedObject *fetchedObject = fetchedObjects.firstObject; if (fetchedObject == nil) { return; } NSNumber *identifierObject = [fetchedObject valueForKey:@"entryIdentifier"]; identifier = identifierObject.unsignedIntegerValue; }; if (performOnQueue) { [viewContext performBlockAndWait:blockToPerform]; } else { blockToPerform(); } return identifier; } #pragma mark - #pragma mark XPC Connection - (id )remoteObjectProxy { return self.serviceConnection.remoteObjectProxy; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/HLSHistoricLogViewContext.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @implementation HLSHistoricLogViewContext @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/HSLHistoricLogProcessDelegate.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @implementation HSLHistoricLogProcessDelegate - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { NSXPCInterface *exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HLSHistoricLogServerProtocol)]; [exportedInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:ascending:fetchLimit:limitToDate:withCompletionBlock:) argumentIndex:0 ofReply:YES]; [exportedInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:withUniqueIdentifier:beforeFetchLimit:afterFetchLimit:limitToDate:withCompletionBlock:) argumentIndex:0 ofReply:YES]; [exportedInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:beforeUniqueIdentifier:fetchLimit:limitToDate:withCompletionBlock:) argumentIndex:0 ofReply:YES]; [exportedInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:afterUniqueIdentifier:fetchLimit:limitToDate:withCompletionBlock:) argumentIndex:0 ofReply:YES]; [exportedInterface setClasses:[NSSet setWithObjects:[NSArray class], [TVCLogLineXPC class], nil] forSelector:@selector(fetchEntriesForView:afterUniqueIdentifier:beforeUniqueIdentifier:fetchLimit:withCompletionBlock:) argumentIndex:0 ofReply:YES]; newConnection.exportedInterface = exportedInterface; NSXPCInterface *remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HLSHistoricLogClientProtocol)]; newConnection.remoteObjectInterface = remoteObjectInterface; HLSHistoricLogProcessMain *exportedObject = [[HLSHistoricLogProcessMain alloc] initWithConnection:newConnection]; newConnection.exportedObject = exportedObject; [newConnection resume]; return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/Headers/Private/HLSHistoricLogLineEntityMigrationPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface HLSHistoricLogLineEntityMigration : NSEntityMigrationPolicy @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/Headers/Private/HLSHistoricLogProcessMainPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface HLSHistoricLogProcessMain : NSObject - (instancetype)initWithConnection:(NSXPCConnection *)connection; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/Headers/Private/HLSHistoricLogProtocol.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* *** XPC PROTOCOL HEADERS ARE PRIVATE *** */ NS_ASSUME_NONNULL_BEGIN @class TVCLogLineXPC; #pragma mark - #pragma mark Server Protocol @protocol HLSHistoricLogServerProtocol - (void)openDatabaseInDirectory:(NSString *)databaseDirectory withCompletionBlock:(void (NS_NOESCAPE ^ _Nullable)(BOOL))completionBlock; - (void)writeLogLine:(TVCLogLineXPC *)logLine; - (void)saveDataWithCompletionBlock:(void (NS_NOESCAPE ^ _Nullable)(void))completionBlock; - (void)forgetView:(NSString *)viewId; - (void)resetDataForView:(NSString *)viewId; - (void)fetchEntriesForView:(NSString *)viewId ascending:(BOOL)ascending fetchLimit:(NSUInteger)fetchLimit // optional (0 == no limit) limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock; - (void)fetchEntriesForView:(NSString *)viewId withUniqueIdentifier:(NSString *)uniqueId beforeFetchLimit:(NSUInteger)fetchLimitBefore // optional (0 == only uniqueId) afterFetchLimit:(NSUInteger)fetchLimitAfter // optional (0 == only uniqueId) limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock; - (void)fetchEntriesForView:(NSString *)viewId beforeUniqueIdentifier:(NSString *)uniqueId fetchLimit:(NSUInteger)fetchLimit // required (> 0) limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock; - (void)fetchEntriesForView:(NSString *)viewId afterUniqueIdentifier:(NSString *)uniqueId fetchLimit:(NSUInteger)fetchLimit // required (> 0) limitToDate:(nullable NSDate *)limitToDate withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock; - (void)fetchEntriesForView:(NSString *)viewId afterUniqueIdentifier:(NSString *)uniqueIdAfter beforeUniqueIdentifier:(NSString *)uniqueIdBefore fetchLimit:(NSUInteger)fetchLimit // optional (0 == no limit) withCompletionBlock:(void (NS_NOESCAPE ^)(NSArray *entries))completionBlock; - (void)setMaximumLineCount:(NSUInteger)maximumLineCount; @end #pragma mark - #pragma mark Client Protocol @protocol HLSHistoricLogClientProtocol - (void)willDeleteUniqueIdentifiers:(NSArray *)uniqueIdentifiers inView:(NSString *)viewId; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/Headers/Private/HLSHistoricLogViewContextPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface HLSHistoricLogViewContext : NSManagedObjectContext @property (nonatomic, copy) NSString *hls_viewId; @property (nonatomic, assign) NSUInteger hls_totalLineCount; @property (nonatomic, assign) NSUInteger hls_newestIdentifier; @property (nonatomic, strong, nullable) dispatch_source_t hls_resizeTimer; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/Headers/Private/HSLHistoricLogPCHPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import #import "StaticDefinitions.h" #import "TVCLogLineXPCPrivate.h" #import "HLSHistoricLogProtocol.h" #import "HLSHistoricLogLineEntityMigrationPrivate.h" #import "HLSHistoricLogViewContextPrivate.h" #import "HLSHistoricLogProcessMainPrivate.h" #import "HSLHistoricLogProcessDelegatePrivate.h" ================================================ FILE: XPC Services/Historic Log File Manager/Classes/Headers/Private/HSLHistoricLogProcessDelegatePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface HSLHistoricLogProcessDelegate : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Classes/main.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2016 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnullability-inferred-on-nested-type" int main(int argc, const char *argv[]) { HSLHistoricLogProcessDelegate *delegate = [HSLHistoricLogProcessDelegate new]; NSXPCListener *listener = [NSXPCListener serviceListener]; listener.delegate = delegate; [listener resume]; return 0; } #pragma clang diagnostic pop NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Historic Log File Manager/Configurations/Sandbox/Release.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.application-groups 8482Q6EPL6.com.codeux.apps.textual ================================================ FILE: XPC Services/Historic Log File Manager/Historic Log File Manager.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C469E4F20EC5E5300094EA4 /* HSLHistoricLogProcessDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469E4220EC5E5300094EA4 /* HSLHistoricLogProcessDelegate.m */; }; 4C469E5020EC5E5300094EA4 /* HLSHistoricLogViewContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469E4320EC5E5300094EA4 /* HLSHistoricLogViewContext.m */; }; 4C469E5120EC5E5300094EA4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469E4420EC5E5300094EA4 /* main.m */; }; 4C469E5220EC5E5300094EA4 /* HLSHistoricLogProcessMain.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469E4D20EC5E5300094EA4 /* HLSHistoricLogProcessMain.m */; }; 4C469E5320EC5E5300094EA4 /* HLSHistoricLogLineEntityMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469E4E20EC5E5300094EA4 /* HLSHistoricLogLineEntityMigration.m */; }; 4C469E9620EC5ED500094EA4 /* TVCLogLineXPC.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469E9520EC5ED500094EA4 /* TVCLogLineXPC.m */; }; 4C469E9C20EC5F7700094EA4 /* HistoricLogFileStorageModel.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 4C469E9820EC5F7700094EA4 /* HistoricLogFileStorageModel.xcmappingmodel */; }; 4C469E9D20EC5F7700094EA4 /* HistoricLogFileStorageModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 4C469E9920EC5F7700094EA4 /* HistoricLogFileStorageModel.xcdatamodeld */; }; 4C58C6E21E15428300934680 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C58C6E11E15428300934680 /* CoreData.framework */; }; 4CBDFBEA2C3ED3920003B272 /* TPCPreferencesUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CBDFBE72C3ED3920003B272 /* TPCPreferencesUserDefaults.m */; }; 4CE1461E20EC10F4000E01C9 /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CE145E420EC0F5C000E01C9 /* CocoaExtensions.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C442C881E13B3D200B187F4 /* Scrollback History Manager.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = "Scrollback History Manager.xpc"; sourceTree = BUILT_PRODUCTS_DIR; }; 4C469E4220EC5E5300094EA4 /* HSLHistoricLogProcessDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSLHistoricLogProcessDelegate.m; sourceTree = ""; }; 4C469E4320EC5E5300094EA4 /* HLSHistoricLogViewContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HLSHistoricLogViewContext.m; sourceTree = ""; }; 4C469E4420EC5E5300094EA4 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4C469E4720EC5E5300094EA4 /* HLSHistoricLogViewContextPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HLSHistoricLogViewContextPrivate.h; sourceTree = ""; }; 4C469E4820EC5E5300094EA4 /* HSLHistoricLogProcessDelegatePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSLHistoricLogProcessDelegatePrivate.h; sourceTree = ""; }; 4C469E4920EC5E5300094EA4 /* HLSHistoricLogProcessMainPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HLSHistoricLogProcessMainPrivate.h; sourceTree = ""; }; 4C469E4A20EC5E5300094EA4 /* HLSHistoricLogLineEntityMigrationPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HLSHistoricLogLineEntityMigrationPrivate.h; sourceTree = ""; }; 4C469E4B20EC5E5300094EA4 /* HSLHistoricLogPCHPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSLHistoricLogPCHPrivate.h; sourceTree = ""; }; 4C469E4D20EC5E5300094EA4 /* HLSHistoricLogProcessMain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HLSHistoricLogProcessMain.m; sourceTree = ""; }; 4C469E4E20EC5E5300094EA4 /* HLSHistoricLogLineEntityMigration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HLSHistoricLogLineEntityMigration.m; sourceTree = ""; }; 4C469E7520EC5E9C00094EA4 /* TVCLogLineXPCPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TVCLogLineXPCPrivate.h; sourceTree = ""; }; 4C469E9520EC5ED500094EA4 /* TVCLogLineXPC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TVCLogLineXPC.m; sourceTree = ""; }; 4C469E9820EC5F7700094EA4 /* HistoricLogFileStorageModel.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = HistoricLogFileStorageModel.xcmappingmodel; sourceTree = ""; }; 4C469E9A20EC5F7700094EA4 /* LogControllerStorageModel (model 2).xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "LogControllerStorageModel (model 2).xcdatamodel"; sourceTree = ""; }; 4C469E9B20EC5F7700094EA4 /* LogControllerStorageModel (model 3).xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "LogControllerStorageModel (model 3).xcdatamodel"; sourceTree = ""; }; 4C469E9F20EC5FA000094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C46A01120EC64FA00094EA4 /* HLSHistoricLogProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HLSHistoricLogProtocol.h; path = Classes/Headers/Private/HLSHistoricLogProtocol.h; sourceTree = SOURCE_ROOT; }; 4C58C6E11E15428300934680 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 4CBDFBE42C3ED32A0003B272 /* TPCPreferencesUserDefaultsPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TPCPreferencesUserDefaultsPrivate.h; sourceTree = ""; }; 4CBDFBE72C3ED3920003B272 /* TPCPreferencesUserDefaults.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TPCPreferencesUserDefaults.m; sourceTree = ""; }; 4CE145E420EC0F5C000E01C9 /* CocoaExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaExtensions.framework; path = "../../.tmp/SharedBuildProducts-Frameworks/CocoaExtensions.framework"; sourceTree = SOURCE_ROOT; }; 4CE145EB20EC0F7F000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE145EE20EC0F7F000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE145F020EC0F7F000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE145FE20EC0F7F000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE145FF20EC0F7F000E01C9 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4CE1460220EC0F7F000E01C9 /* Foundation.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4CE1460320EC0F7F000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE1460520EC0F7F000E01C9 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4CE1461320EC0F7F000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1461620EC0F7F000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE1461820EC0F7F000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE1461C20EC0FAF000E01C9 /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 4C442C851E13B3D200B187F4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4C58C6E21E15428300934680 /* CoreData.framework in Frameworks */, 4CE1461E20EC10F4000E01C9 /* CocoaExtensions.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4C442C7F1E13B3D200B187F4 = { isa = PBXGroup; children = ( 4C469E4120EC5E5300094EA4 /* Classes */, 4C442CA81E13B42900B187F4 /* Resources */, 4C442CBE1E13B77200B187F4 /* Frameworks */, 4C442C891E13B3D200B187F4 /* Products */, ); sourceTree = ""; }; 4C442C891E13B3D200B187F4 /* Products */ = { isa = PBXGroup; children = ( 4C442C881E13B3D200B187F4 /* Scrollback History Manager.xpc */, ); name = Products; sourceTree = ""; }; 4C442CA81E13B42900B187F4 /* Resources */ = { isa = PBXGroup; children = ( 4C761BE21E1565D800A505B7 /* Build Settings */, 4C469E9720EC5F7700094EA4 /* Data Models */, 4C469E9E20EC5FA000094EA4 /* Property Lists */, ); path = Resources; sourceTree = ""; }; 4C442CAF1E13B55900B187F4 /* Configurations */ = { isa = PBXGroup; children = ( 4CE145E720EC0F7F000E01C9 /* Build */, 4CE1461920EC0F7F000E01C9 /* Sandbox */, ); name = Configurations; path = ../../Configurations; sourceTree = SOURCE_ROOT; }; 4C442CBE1E13B77200B187F4 /* Frameworks */ = { isa = PBXGroup; children = ( 4C58C6D61E15372B00934680 /* External Frameworks */, 4C560E291E1558BA00D3ACB9 /* System Frameworks */, ); name = Frameworks; sourceTree = ""; }; 4C469E4120EC5E5300094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C469E5420EC5E9C00094EA4 /* Shared */, 4C469E4520EC5E5300094EA4 /* Headers */, 4C469E4E20EC5E5300094EA4 /* HLSHistoricLogLineEntityMigration.m */, 4C469E4D20EC5E5300094EA4 /* HLSHistoricLogProcessMain.m */, 4C469E4320EC5E5300094EA4 /* HLSHistoricLogViewContext.m */, 4C469E4220EC5E5300094EA4 /* HSLHistoricLogProcessDelegate.m */, 4C469E4420EC5E5300094EA4 /* main.m */, ); path = Classes; sourceTree = ""; }; 4C469E4520EC5E5300094EA4 /* Headers */ = { isa = PBXGroup; children = ( 4C469E4620EC5E5300094EA4 /* Private */, ); path = Headers; sourceTree = ""; }; 4C469E4620EC5E5300094EA4 /* Private */ = { isa = PBXGroup; children = ( 4C469E4A20EC5E5300094EA4 /* HLSHistoricLogLineEntityMigrationPrivate.h */, 4C469E4920EC5E5300094EA4 /* HLSHistoricLogProcessMainPrivate.h */, 4C469E4720EC5E5300094EA4 /* HLSHistoricLogViewContextPrivate.h */, 4C469E4B20EC5E5300094EA4 /* HSLHistoricLogPCHPrivate.h */, 4C469E4820EC5E5300094EA4 /* HSLHistoricLogProcessDelegatePrivate.h */, ); path = Private; sourceTree = ""; }; 4C469E5420EC5E9C00094EA4 /* Shared */ = { isa = PBXGroup; children = ( 4CBDFBE82C3ED3920003B272 /* Preferences */, 4C469E6320EC5E9C00094EA4 /* Headers */, 4C469E9320EC5ED500094EA4 /* Views */, ); name = Shared; path = ../../Sources/Shared; sourceTree = SOURCE_ROOT; }; 4C469E6320EC5E9C00094EA4 /* Headers */ = { isa = PBXGroup; children = ( 4C46A00E20EC64FA00094EA4 /* Services */, 4C469E7120EC5E9C00094EA4 /* Private */, ); path = Headers; sourceTree = ""; }; 4C469E7120EC5E9C00094EA4 /* Private */ = { isa = PBXGroup; children = ( 4CBDFBE42C3ED32A0003B272 /* TPCPreferencesUserDefaultsPrivate.h */, 4C469E7520EC5E9C00094EA4 /* TVCLogLineXPCPrivate.h */, ); path = Private; sourceTree = ""; }; 4C469E9320EC5ED500094EA4 /* Views */ = { isa = PBXGroup; children = ( 4C469E9420EC5ED500094EA4 /* Channel View */, ); path = Views; sourceTree = ""; }; 4C469E9420EC5ED500094EA4 /* Channel View */ = { isa = PBXGroup; children = ( 4C469E9520EC5ED500094EA4 /* TVCLogLineXPC.m */, ); path = "Channel View"; sourceTree = ""; }; 4C469E9720EC5F7700094EA4 /* Data Models */ = { isa = PBXGroup; children = ( 4C469E9820EC5F7700094EA4 /* HistoricLogFileStorageModel.xcmappingmodel */, 4C469E9920EC5F7700094EA4 /* HistoricLogFileStorageModel.xcdatamodeld */, ); path = "Data Models"; sourceTree = ""; }; 4C469E9E20EC5FA000094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C469E9F20EC5FA000094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A00E20EC64FA00094EA4 /* Services */ = { isa = PBXGroup; children = ( 4C46A01120EC64FA00094EA4 /* HLSHistoricLogProtocol.h */, ); path = Services; sourceTree = ""; }; 4C560E291E1558BA00D3ACB9 /* System Frameworks */ = { isa = PBXGroup; children = ( 4C58C6E11E15428300934680 /* CoreData.framework */, ); name = "System Frameworks"; sourceTree = ""; }; 4C58C6D61E15372B00934680 /* External Frameworks */ = { isa = PBXGroup; children = ( 4CE145E420EC0F5C000E01C9 /* CocoaExtensions.framework */, ); name = "External Frameworks"; sourceTree = ""; }; 4C761BE21E1565D800A505B7 /* Build Settings */ = { isa = PBXGroup; children = ( 4C442CAF1E13B55900B187F4 /* Configurations */, ); name = "Build Settings"; sourceTree = ""; }; 4CBDFBE82C3ED3920003B272 /* Preferences */ = { isa = PBXGroup; children = ( 4CBDFBE72C3ED3920003B272 /* TPCPreferencesUserDefaults.m */, ); path = Preferences; sourceTree = ""; }; 4CE145E720EC0F7F000E01C9 /* Build */ = { isa = PBXGroup; children = ( 4CE145FB20EC0F7F000E01C9 /* Common */, 4CE1461020EC0F7F000E01C9 /* Debug */, 4CE145E820EC0F7F000E01C9 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4CE145E820EC0F7F000E01C9 /* Standard Release */ = { isa = PBXGroup; children = ( 4CE145F020EC0F7F000E01C9 /* Enabled Features.xcconfig */, 4CE145EB20EC0F7F000E01C9 /* Textual.xcconfig */, 4CE145EE20EC0F7F000E01C9 /* XPC Services.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4CE145FB20EC0F7F000E01C9 /* Common */ = { isa = PBXGroup; children = ( 4CE1460520EC0F7F000E01C9 /* Foundation Debug.xcconfig */, 4CE1460220EC0F7F000E01C9 /* Foundation.xcconfig */, 4CE145FF20EC0F7F000E01C9 /* Preserve Symbols.xcconfig */, 4CE145FE20EC0F7F000E01C9 /* Textual.xcconfig */, 4CE1460320EC0F7F000E01C9 /* XPC Services.xcconfig */, ); path = Common; sourceTree = ""; }; 4CE1461020EC0F7F000E01C9 /* Debug */ = { isa = PBXGroup; children = ( 4CE1461820EC0F7F000E01C9 /* Enabled Features.xcconfig */, 4CE1461320EC0F7F000E01C9 /* Textual.xcconfig */, 4CE1461620EC0F7F000E01C9 /* XPC Services.xcconfig */, ); path = Debug; sourceTree = ""; }; 4CE1461920EC0F7F000E01C9 /* Sandbox */ = { isa = PBXGroup; children = ( 4CE1461C20EC0FAF000E01C9 /* Release.entitlements */, ); name = Sandbox; path = Configurations/Sandbox; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 4C442C871E13B3D200B187F4 /* Historic Log File Manager */ = { isa = PBXNativeTarget; buildConfigurationList = 4C442C941E13B3D200B187F4 /* Build configuration list for PBXNativeTarget "Historic Log File Manager" */; buildPhases = ( 4C442C861E13B3D200B187F4 /* Resources */, 4C442C851E13B3D200B187F4 /* Frameworks */, 4C442C841E13B3D200B187F4 /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "Historic Log File Manager"; productName = "Historic Log File Manager"; productReference = 4C442C881E13B3D200B187F4 /* Scrollback History Manager.xpc */; productType = "com.apple.product-type.xpc-service"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 4C442C801E13B3D200B187F4 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Codeux Software, LLC"; TargetAttributes = { 4C442C871E13B3D200B187F4 = { CreatedOnToolsVersion = 8.0; DevelopmentTeam = 8482Q6EPL6; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 4C442C831E13B3D200B187F4 /* Build configuration list for PBXProject "Historic Log File Manager" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 4C442C7F1E13B3D200B187F4; productRefGroup = 4C442C891E13B3D200B187F4 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4C442C871E13B3D200B187F4 /* Historic Log File Manager */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4C442C861E13B3D200B187F4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 4C442C841E13B3D200B187F4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C469E5020EC5E5300094EA4 /* HLSHistoricLogViewContext.m in Sources */, 4C469E5320EC5E5300094EA4 /* HLSHistoricLogLineEntityMigration.m in Sources */, 4C469E5220EC5E5300094EA4 /* HLSHistoricLogProcessMain.m in Sources */, 4C469E9620EC5ED500094EA4 /* TVCLogLineXPC.m in Sources */, 4C469E4F20EC5E5300094EA4 /* HSLHistoricLogProcessDelegate.m in Sources */, 4C469E9D20EC5F7700094EA4 /* HistoricLogFileStorageModel.xcdatamodeld in Sources */, 4CBDFBEA2C3ED3920003B272 /* TPCPreferencesUserDefaults.m in Sources */, 4C469E5120EC5E5300094EA4 /* main.m in Sources */, 4C469E9C20EC5F7700094EA4 /* HistoricLogFileStorageModel.xcmappingmodel in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 4C442C921E13B3D200B187F4 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE1461620EC0F7F000E01C9 /* XPC Services.xcconfig */; buildSettings = { }; name = Debug; }; 4C442C931E13B3D200B187F4 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE145EE20EC0F7F000E01C9 /* XPC Services.xcconfig */; buildSettings = { }; name = Release; }; 4C442C951E13B3D200B187F4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "${PROJECT_DIR}/Configurations/Sandbox/Release.entitlements"; GCC_PREFIX_HEADER = "${SRCROOT}/Classes/Headers/Private/HSLHistoricLogPCHPrivate.h"; INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-utilities.Textual-ScrollbackHistoryManager"; PRODUCT_NAME = "Scrollback History Manager"; }; name = Debug; }; 4C442C961E13B3D200B187F4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "${PROJECT_DIR}/Configurations/Sandbox/Release.entitlements"; GCC_PREFIX_HEADER = "${SRCROOT}/Classes/Headers/Private/HSLHistoricLogPCHPrivate.h"; INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-utilities.Textual-ScrollbackHistoryManager"; PRODUCT_NAME = "Scrollback History Manager"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 4C442C831E13B3D200B187F4 /* Build configuration list for PBXProject "Historic Log File Manager" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C442C921E13B3D200B187F4 /* Debug */, 4C442C931E13B3D200B187F4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4C442C941E13B3D200B187F4 /* Build configuration list for PBXNativeTarget "Historic Log File Manager" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C442C951E13B3D200B187F4 /* Debug */, 4C442C961E13B3D200B187F4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ 4C469E9920EC5F7700094EA4 /* HistoricLogFileStorageModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( 4C469E9A20EC5F7700094EA4 /* LogControllerStorageModel (model 2).xcdatamodel */, 4C469E9B20EC5F7700094EA4 /* LogControllerStorageModel (model 3).xcdatamodel */, ); currentVersion = 4C469E9B20EC5F7700094EA4 /* LogControllerStorageModel (model 3).xcdatamodel */; path = HistoricLogFileStorageModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; /* End XCVersionGroup section */ }; rootObject = 4C442C801E13B3D200B187F4 /* Project object */; } ================================================ FILE: XPC Services/Historic Log File Manager/Resources/Data Models/HistoricLogFileStorageModel.xcdatamodeld/.xccurrentversion ================================================ _XCCurrentVersionName LogControllerStorageModel (model 3).xcdatamodel ================================================ FILE: XPC Services/Historic Log File Manager/Resources/Data Models/HistoricLogFileStorageModel.xcdatamodeld/LogControllerStorageModel (model 2).xcdatamodel/contents ================================================ ================================================ FILE: XPC Services/Historic Log File Manager/Resources/Data Models/HistoricLogFileStorageModel.xcdatamodeld/LogControllerStorageModel (model 3).xcdatamodel/contents ================================================ ================================================ FILE: XPC Services/Historic Log File Manager/Resources/Data Models/HistoricLogFileStorageModel.xcmappingmodel/xcmapping.xml ================================================ 134481920 031BCA3F-445F-483C-93E8-91708E39F5A1 122 NSPersistenceFrameworkVersion 847 NSStoreModelVersionHashes XDDevAttributeMapping 0plcXXRN7XHKl5CcF+fwriFmUpON3ZtcI/AfK748aWc= XDDevEntityMapping qeN1Ym3TkWN1G6dU9RfX6Kd2ccEvcDVWHpd3LpLgboI= XDDevMappingModel EqtMzvRnVZWkXwBHu4VeVGy8UyoOe+bi67KC79kphlQ= XDDevPropertyMapping XN33V44TTGY4JETlMoOB5yyTKxB+u4slvDIinv0rtGA= XDDevRelationshipMapping akYY9LhehVA/mCb4ATLWuI9XGLcjpm14wWL1oEBtIcs= NSStoreModelVersionHashesVersion 3 NSStoreModelVersionIdentifiers YnBsaXN0MDDUAQIDBAUGLC1YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkH CBMUGRoiJilVJG51bGzVCQoLDA0ODxAREllOU09wZXJhbmReTlNTZWxlY3Rvck5hbWVfEBBOU0V4cHJlc3Npb25UeXBlW05TQXJndW1lbnRzViRjbGFzc4ADgAIQBIAGgAhfEBpuZXdMb2dMaW5lVW5pcXVlSWRlbnRpZmllctMVCw0WFxhaTlNWYXJpYWJsZYAEEAKABVxlbnRpdHlQb2xpY3nSGxwdHlokY2xhc3NuYW1lWCRjbGFzc2VzXxAUTlNWYXJpYWJsZUV4cHJlc3Npb26jHyAhXxAUTlNWYXJpYWJsZUV4cHJlc3Npb25cTlNFeHByZXNzaW9uWE5TT2JqZWN00iMNJCVaTlMub2JqZWN0c6CAB9IbHCcoV05TQXJyYXmiJyHSGxwqK18QFE5TRnVuY3Rpb25FeHByZXNzaW9uoyogIV8QD05TS2V5ZWRBcmNoaXZlctEuL1Ryb290gAEACAARABoAIwAtADIANwBBAEcAUgBcAGsAfgCKAJEAkwCVAJcAmQCbALgAvwDKAMwAzgDQAN0A4gDtAPYBDQERASgBNQE+AUMBTgFPAVEBVgFeAWEBZgF9AYEBkwGWAZsAAAAAAAACAQAAAAAAAAAwAAAAAAAAAAAAAAAAAAABnQ== logLineUniqueIdentifier Resources/Data Models/HistoricLogFileStorageModel.xcdatamodeld/LogControllerStorageModel (model 2).xcdatamodel YnBsaXN0MDDUAAEAAgADAAQABQAGBrcGuFgkdmVyc2lvblgkb2JqZWN0c1kkYXJjaGl2ZXJUJHRv cBIAAYagrxCyAAcACAAXADMANAA1AD0APgBZAFoAWwBhAGIAbgCEAIUAhgCHAIgAiQCKAIsAjACNAKYAqQCwALYAxQDUANcA5gD1APgAWAEIARcBGwEfAS4BNAE1AT0BTAFNAVYBYgFjAWQBZQFmAXsBfAGEAYUBhgGSAaYBpwGoAakBqgGrAawBrQGuAb0BzAHbAd8B7gH9Af4CDQIcAisCNwJJAkoCSwJMAk0CTgJPAlACXwJgAm8CfgKNAo4CnQKsArsCwwLYAtkC4QLtAwEDEAMfAy4DMgNBA1ADXwNuA30DiQObA6oDuQPIA9cD2APnA/YEBQQaBBsEIwQvBEMEUgRhBHAEdASDBJIEoQSwBL8EywTdBOwE7QT8BQsFGgUbBSoFOQVIBV0FXgVmBXIFhgWVBaQFswW3BcYF1QXkBfMGAgYOBiAGLwY+Bk0GXAZdBmwGewaKBosGjgaXBpsGnwajBqsGrgayBrNVJG51bGzXAAkACgALAAwADQAOAA8AEAARABIAEwAUABMAFl8QD194ZF9yb290UGFja2FnZVYkY2xhc3NcX3hkX2NvbW1lbnRzXxAQX3hkX21vZGVsTWFuYWdlcl8QFV9jb25maWd1cmF0aW9uc0J5TmFtZV1feGRfbW9kZWxOYW1lXxAXX21vZGVsVmVyc2lvbklkZW50aWZpZXKAAoCxgK6AAICvgACAsN4AGAAZABoAGwAcAB0AHgAKAB8AIAAhACIAIwAkACUAJgAnACgAJQATACsALAAtAC4ALwAlACUAE18QHFhEQnVja2V0Rm9yQ2xhc3Nlc3dhc0VuY29kZWRfEBpYREJ1Y2tldEZvclBhY2thZ2Vzc3RvcmFnZV8QHFhEQnVja2V0Rm9ySW50ZXJmYWNlc3N0b3JhZ2VfEA9feGRfb3duaW5nTW9kZWxfEB1YREJ1Y2tldEZvclBhY2thZ2Vzd2FzRW5jb2RlZFZfb3duZXJfEBtYREJ1Y2tldEZvckRhdGFUeXBlc3N0b3JhZ2VbX3Zpc2liaWxpdHlfEBlYREJ1Y2tldEZvckNsYXNzZXNzdG9yYWdlVV9uYW1lXxAfWERCdWNrZXRGb3JJbnRlcmZhY2Vzd2FzRW5jb2RlZF8QHlhEQnVja2V0Rm9yRGF0YVR5cGVzd2FzRW5jb2RlZF8QEF91bmlxdWVFbGVtZW50SUSABICsgKqAAYAEgACAq4CtEACABYADgASABIAAUFNZRVPTADYANwAKADgAOgA8V05TLmtleXNaTlMub2JqZWN0c6EAOYAGoQA7gAeAJVdMb2dMaW5l3xAQAD8AQABBAEIAHQBDAEQAHwBFAEYACgAhAEcASAAkAEkASgBLACUAJQAQAE8AUAAtACUASgBTADkASgBWAFcAWF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc2R1cGxpY2F0ZXNfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zc3RvcmFnZVtfaXNBYnN0cmFjdIAJgC2ABIAEgAKACoCngASACYCpgAaACYCogAgIErmKi6pXb3JkZXJlZNMANgA3AAoAXABeADyhAF2AC6EAX4AMgCVeWERfUFN0ZXJlb3R5cGXZAB0AIQBjAAoAJABkAB8ASQBlADsAXQBKAGkAEwAlAC0AWABtXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgAeAC4AJgCyAAIAECIAN0wA2ADcACgBvAHkAPKkAcABxAHIAcwB0AHUAdgB3AHiADoAPgBCAEYASgBOAFIAVgBapAHoAewB8AH0AfgB/AIAAgQCCgBeAG4AcgB6AH4AhgCOAJoAqgCVfEBNYRFBNQ29tcG91bmRJbmRleGVzXxAQWERfUFNLX2VsZW1lbnRJRF8QGVhEUE1VbmlxdWVuZXNzQ29uc3RyYWludHNfEBpYRF9QU0tfdmVyc2lvbkhhc2hNb2RpZmllcl8QGVhEX1BTS19mZXRjaFJlcXVlc3RzQXJyYXlfEBFYRF9QU0tfaXNBYnN0cmFjdF8QD1hEX1BTS191c2VySW5mb18QE1hEX1BTS19jbGFzc01hcHBpbmdfEBZYRF9QU0tfZW50aXR5Q2xhc3NOYW1l3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAmQATAF8AWABYAFgALQBYAKAAcABYAFgAEwBYVV90eXBlWF9kZWZhdWx0XF9hc3NvY2lhdGlvbltfaXNSZWFkT25seVlfaXNTdGF0aWNZX2lzVW5pcXVlWl9pc0Rlcml2ZWRaX2lzT3JkZXJlZFxfaXNDb21wb3NpdGVXX2lzTGVhZoAAgBiAAIAMCAgICIAagA4ICIAACNIANwAKAKcAqKCAGdIAqgCrAKwArVokY2xhc3NuYW1lWCRjbGFzc2VzXk5TTXV0YWJsZUFycmF5owCsAK4Ar1dOU0FycmF5WE5TT2JqZWN00gCqAKsAsQCyXxAQWERVTUxQcm9wZXJ0eUltcKQAswC0ALUAr18QEFhEVU1MUHJvcGVydHlJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwBfAFgAWABYAC0AWACgAHEAWABYABMAWIAAgACAAIAMCAgICIAagA8ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAMcAEwBfAFgAWABYAC0AWACgAHIAWABYABMAWIAAgB2AAIAMCAgICIAagBAICIAACNIANwAKANUAqKCAGd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwBfAFgAWABYAC0AWACgAHMAWABYABMAWIAAgACAAIAMCAgICIAagBEICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAOgAEwBfAFgAWABYAC0AWACgAHQAWABYABMAWIAAgCCAAIAMCAgICIAagBIICIAACNIANwAKAPYAqKCAGd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAPoAEwBfAFgAWABYAC0AWACgAHUAWABYABMAWIAAgCKAAIAMCAgICIAagBMICIAACAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEKABMAXwBYAFgAWAAtAFgAoAB2AFgAWAATAFiAAIAkgACADAgICAiAGoAUCAiAAAjTADYANwAKARgBGQA8oKCAJdIAqgCrARwBHV8QE05TTXV0YWJsZURpY3Rpb25hcnmjARwBHgCvXE5TRGljdGlvbmFyed8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASEAEwBfAFgAWABYAC0AWACgAHcAWABYABMAWIAAgCeAAIAMCAgICIAagBUICIAACNYAIQAKACQASQAdAB8BLwEwABMAWAATAC2AKIApgAAIgABfEBRYREdlbmVyaWNSZWNvcmRDbGFzc9IAqgCrATYBN11YRFVNTENsYXNzSW1wpgE4ATkBOgE7ATwAr11YRFVNTENsYXNzSW1wXxASWERVTUxDbGFzc2lmaWVySW1wXxARWERVTUxOYW1lc3BhY2VJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAT8AEwBfAFgAWABYAC0AWACgAHgAWABYABMAWIAAgCuAAIAMCAgICIAagBYICIAACF8QD05TTWFuYWdlZE9iamVjdNIAqgCrAU4BT18QElhEVU1MU3RlcmVvdHlwZUltcKcBUAFRAVIBUwFUAVUAr18QElhEVU1MU3RlcmVvdHlwZUltcF1YRFVNTENsYXNzSW1wXxASWERVTUxDbGFzc2lmaWVySW1wXxARWERVTUxOYW1lc3BhY2VJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcNMANgA3AAoBVwFcADykAVgBWQFaAVuALoAvgDCAMaQBXQFeAV8BYIAygF6AdoCPgCVcY3JlYXRpb25EYXRlVGRhdGFSaWRZY2hhbm5lbElk3xASAI4AjwCQAWcAHQCSAJMBaAAfAJEBaQCUAAoAIQCVAJYAJACXABMAEwATACUAOwBYAFgBcQAtAFgASgBYAXUBWABYAFgBeQBYXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASABwgIgDQIgAkIgF2ALggIgDMIEmSVafvTADYANwAKAX0BgAA8ogF+AX+ANYA2ogGBAYKAN4BLgCVfEBJYRF9QUHJvcFN0ZXJlb3R5cGVfEBJYRF9QQXR0X1N0ZXJlb3R5cGXZAB0AIQGHAAoAJAGIAB8ASQGJAV0BfgBKAGkAEwAlAC0AWAGRXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgDKANYAJgCyAAIAECIA40wA2ADcACgGTAZwAPKgBlAGVAZYBlwGYAZkBmgGbgDmAOoA7gDyAPYA+gD+AQKgBnQGeAZ8BoAGhAaIBowGkgEGAQoBDgEWARoBIgEmASoAlXxAbWERfUFBTS19pc1N0b3JlZEluVHJ1dGhGaWxlXxAbWERfUFBTS192ZXJzaW9uSGFzaE1vZGlmaWVyXxAQWERfUFBTS191c2VySW5mb18QEVhEX1BQU0tfaXNJbmRleGVkXxASWERfUFBTS19pc09wdGlvbmFsXxAaWERfUFBTS19pc1Nwb3RsaWdodEluZGV4ZWRfEBFYRF9QUFNLX2VsZW1lbnRJRF8QE1hEX1BQU0tfaXNUcmFuc2llbnTfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMBgQBYAFgAWAAtAFgAoAGUAFgAWAATAFiAAIAigACANwgICAiAGoA5CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMBgQBYAFgAWAAtAFgAoAGVAFgAWAATAFiAAIAAgACANwgICAiAGoA6CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwHOABMBgQBYAFgAWAAtAFgAoAGWAFgAWAATAFiAAIBEgACANwgICAiAGoA7CAiAAAjTADYANwAKAdwB3QA8oKCAJd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAPoAEwGBAFgAWABYAC0AWACgAZcAWABYABMAWIAAgCKAAIA3CAgICIAagDwICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAfAAEwGBAFgAWABYAC0AWACgAZgAWABYABMAWIAAgEeAAIA3CAgICIAagD0ICIAACAnfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMBgQBYAFgAWAAtAFgAoAGZAFgAWAATAFiAAIAigACANwgICAiAGoA+CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMBgQBYAFgAWAAtAFgAoAGaAFgAWAATAFiAAIAAgACANwgICAiAGoA/CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMBgQBYAFgAWAAtAFgAoAGbAFgAWAATAFiAAIAigACANwgICAiAGoBACAiAAAjZAB0AIQIsAAoAJAItAB8ASQIuAV0BfwBKAGkAEwAlAC0AWAI2XxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgDKANoAJgCyAAIAECIBM0wA2ADcACgI4AkAAPKcCOQI6AjsCPAI9Aj4CP4BNgE6AT4BQgFGAUoBTpwJBAkICQwJEAkUCRgJHgFSAVoBXgFiAWoBbgFyAJV8QHVhEX1BBdHRLX2RlZmF1bHRWYWx1ZUFzU3RyaW5nXxAoWERfUEF0dEtfYWxsb3dzRXh0ZXJuYWxCaW5hcnlEYXRhU3RvcmFnZV8QF1hEX1BBdHRLX21pblZhbHVlU3RyaW5nXxAWWERfUEF0dEtfYXR0cmlidXRlVHlwZV8QF1hEX1BBdHRLX21heFZhbHVlU3RyaW5nXxAdWERfUEF0dEtfdmFsdWVUcmFuc2Zvcm1lck5hbWVfECBYRF9QQXR0S19yZWd1bGFyRXhwcmVzc2lvblN0cmluZ98QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAlIAEwGCAFgAWABYAC0AWACgAjkAWABYABMAWIAAgFWAAIBLCAgICIAagE0ICIAACFMwLjDfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMBggBYAFgAWAAtAFgAoAI6AFgAWAATAFiAAIAigACASwgICAiAGoBOCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMBggBYAFgAWAAtAFgAoAI7AFgAWAATAFiAAIAAgACASwgICAiAGoBPCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwKAABMBggBYAFgAWAAtAFgAoAI8AFgAWAATAFiAAIBZgACASwgICAiAGoBQCAiAAAgRAfTfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMBggBYAFgAWAAtAFgAoAI9AFgAWAATAFiAAIAAgACASwgICAiAGoBRCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMBggBYAFgAWAAtAFgAoAI+AFgAWAATAFiAAIAAgACASwgICAiAGoBSCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMBggBYAFgAWAAtAFgAoAI/AFgAWAATAFiAAIAAgACASwgICAiAGoBTCAiAAAjSAKoAqwK8Ar1dWERQTUF0dHJpYnV0ZaYCvgK/AsACwQLCAK9dWERQTUF0dHJpYnV0ZVxYRFBNUHJvcGVydHlfEBBYRFVNTFByb3BlcnR5SW1wXxAUWERVTUxOYW1lZEVsZW1lbnRJbXBfEA9YRFVNTEVsZW1lbnRJbXDfEBIAjgCPAJACxAAdAJIAkwLFAB8AkQLGAJQACgAhAJUAlgAkAJcAEwATABMAJQA7AFgAWALOAC0AWABKAFgBdQFZAFgAWALWAFhfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIAHCAiAYAiACQiAXYAvCAiAXwgSJ1mbxdMANgA3AAoC2gLdADyiAX4Bf4A1gDaiAt4C34BhgGyAJdkAHQAhAuIACgAkAuMAHwBJAuQBXgF+AEoAaQATACUALQBYAuxfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAXoA1gAmALIAAgAQIgGLTADYANwAKAu4C9wA8qAGUAZUBlgGXAZgBmQGaAZuAOYA6gDuAPIA9gD6AP4BAqAL4AvkC+gL7AvwC/QL+Av+AY4BkgGWAZ4BogGmAaoBrgCXfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMC3gBYAFgAWAAtAFgAoAGUAFgAWAATAFiAAIAigACAYQgICAiAGoA5CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMC3gBYAFgAWAAtAFgAoAGVAFgAWAATAFiAAIAAgACAYQgICAiAGoA6CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwMhABMC3gBYAFgAWAAtAFgAoAGWAFgAWAATAFiAAIBmgACAYQgICAiAGoA7CAiAAAjTADYANwAKAy8DMAA8oKCAJd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAPoAEwLeAFgAWABYAC0AWACgAZcAWABYABMAWIAAgCKAAIBhCAgICIAagDwICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAfAAEwLeAFgAWABYAC0AWACgAZgAWABYABMAWIAAgEeAAIBhCAgICIAagD0ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAPoAEwLeAFgAWABYAC0AWACgAZkAWABYABMAWIAAgCKAAIBhCAgICIAagD4ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwLeAFgAWABYAC0AWACgAZoAWABYABMAWIAAgACAAIBhCAgICIAagD8ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAPoAEwLeAFgAWABYAC0AWACgAZsAWABYABMAWIAAgCKAAIBhCAgICIAagEAICIAACNkAHQAhA34ACgAkA38AHwBJA4ABXgF/AEoAaQATACUALQBYA4hfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAXoA2gAmALIAAgAQIgG3TADYANwAKA4oDkgA8pwI5AjoCOwI8Aj0CPgI/gE2AToBPgFCAUYBSgFOnA5MDlAOVA5YDlwOYA5mAboBvgHCAcYBzgHSAdYAl3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAt8AWABYAFgALQBYAKACOQBYAFgAEwBYgACAAIAAgGwICAgIgBqATQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMA+gATAt8AWABYAFgALQBYAKACOgBYAFgAEwBYgACAIoAAgGwICAgIgBqATggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAt8AWABYAFgALQBYAKACOwBYAFgAEwBYgACAAIAAgGwICAgIgBqATwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMDygATAt8AWABYAFgALQBYAKACPABYAFgAEwBYgACAcoAAgGwICAgIgBqAUAgIgAAIEQPo3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAt8AWABYAFgALQBYAKACPQBYAFgAEwBYgACAAIAAgGwICAgIgBqAUQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAt8AWABYAFgALQBYAKACPgBYAFgAEwBYgACAAIAAgGwICAgIgBqAUggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAt8AWABYAFgALQBYAKACPwBYAFgAEwBYgACAAIAAgGwICAgIgBqAUwgIgAAI3xASAI4AjwCQBAYAHQCSAJMEBwAfAJEECACUAAoAIQCVAJYAJACXABMAEwATACUAOwBYAFgEEAAtAFgASgBYAXUBWgBYAFgEGABYXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASABwgIgHgIgAkIgF2AMAgIgHcIErT/2e3TADYANwAKBBwEHwA8ogF+AX+ANYA2ogQgBCGAeYCEgCXZAB0AIQQkAAoAJAQlAB8ASQQmAV8BfgBKAGkAEwAlAC0AWAQuXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgHaANYAJgCyAAIAECIB60wA2ADcACgQwBDkAPKgBlAGVAZYBlwGYAZkBmgGbgDmAOoA7gDyAPYA+gD+AQKgEOgQ7BDwEPQQ+BD8EQARBgHuAfIB9gH+AgICBgIKAg4Al3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMA+gATBCAAWABYAFgALQBYAKABlABYAFgAEwBYgACAIoAAgHkICAgIgBqAOQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBCAAWABYAFgALQBYAKABlQBYAFgAEwBYgACAAIAAgHkICAgIgBqAOggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMEYwATBCAAWABYAFgALQBYAKABlgBYAFgAEwBYgACAfoAAgHkICAgIgBqAOwgIgAAI0wA2ADcACgRxBHIAPKCggCXfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMEIABYAFgAWAAtAFgAoAGXAFgAWAATAFiAAIAigACAeQgICAiAGoA8CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwHwABMEIABYAFgAWAAtAFgAoAGYAFgAWAATAFiAAIBHgACAeQgICAiAGoA9CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMEIABYAFgAWAAtAFgAoAGZAFgAWAATAFiAAIAigACAeQgICAiAGoA+CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMEIABYAFgAWAAtAFgAoAGaAFgAWAATAFiAAIAAgACAeQgICAiAGoA/CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMEIABYAFgAWAAtAFgAoAGbAFgAWAATAFiAAIAigACAeQgICAiAGoBACAiAAAjZAB0AIQTAAAoAJATBAB8ASQTCAV8BfwBKAGkAEwAlAC0AWATKXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgHaANoAJgCyAAIAECICF0wA2ADcACgTMBNQAPKcCOQI6AjsCPAI9Aj4CP4BNgE6AT4BQgFGAUoBTpwTVBNYE1wTYBNkE2gTbgIaAiICJgIqAjICNgI6AJd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATBN8AEwQhAFgAWABYAC0AWACgAjkAWABYABMAWIAAgIeAAICECAgICIAagE0ICIAACFEw3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMA+gATBCEAWABYAFgALQBYAKACOgBYAFgAEwBYgACAIoAAgIQICAgIgBqATggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBCEAWABYAFgALQBYAKACOwBYAFgAEwBYgACAAIAAgIQICAgIgBqATwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMFDQATBCEAWABYAFgALQBYAKACPABYAFgAEwBYgACAi4AAgIQICAgIgBqAUAgIgAAIEQEs3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBCEAWABYAFgALQBYAKACPQBYAFgAEwBYgACAAIAAgIQICAgIgBqAUQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBCEAWABYAFgALQBYAKACPgBYAFgAEwBYgACAAIAAgIQICAgIgBqAUggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBCEAWABYAFgALQBYAKACPwBYAFgAEwBYgACAAIAAgIQICAgIgBqAUwgIgAAI3xASAI4AjwCQBUkAHQCSAJMFSgAfAJEFSwCUAAoAIQCVAJYAJACXABMAEwATACUAOwBYAFgFUwAtAFgASgBYAXUBWwBYAFgFWwBYXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASABwgIgJEIgAkIgF2AMQgIgJAIEniZwYPTADYANwAKBV8FYgA8ogF+AX+ANYA2ogVjBWSAkoCdgCXZAB0AIQVnAAoAJAVoAB8ASQVpAWABfgBKAGkAEwAlAC0AWAVxXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgI+ANYAJgCyAAIAECICT0wA2ADcACgVzBXwAPKgBlAGVAZYBlwGYAZkBmgGbgDmAOoA7gDyAPYA+gD+AQKgFfQV+BX8FgAWBBYIFgwWEgJSAlYCWgJiAmYCagJuAnIAl3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMA+gATBWMAWABYAFgALQBYAKABlABYAFgAEwBYgACAIoAAgJIICAgIgBqAOQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBWMAWABYAFgALQBYAKABlQBYAFgAEwBYgACAAIAAgJIICAgIgBqAOggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMFpgATBWMAWABYAFgALQBYAKABlgBYAFgAEwBYgACAl4AAgJIICAgIgBqAOwgIgAAI0wA2ADcACgW0BbUAPKCggCXfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMFYwBYAFgAWAAtAFgAoAGXAFgAWAATAFiAAIAigACAkggICAiAGoA8CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwHwABMFYwBYAFgAWAAtAFgAoAGYAFgAWAATAFiAAIBHgACAkggICAiAGoA9CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMFYwBYAFgAWAAtAFgAoAGZAFgAWAATAFiAAIAigACAkggICAiAGoA+CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMFYwBYAFgAWAAtAFgAoAGaAFgAWAATAFiAAIAAgACAkggICAiAGoA/CAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwD6ABMFYwBYAFgAWAAtAFgAoAGbAFgAWAATAFiAAIAigACAkggICAiAGoBACAiAAAjZAB0AIQYDAAoAJAYEAB8ASQYFAWABfwBKAGkAEwAlAC0AWAYNXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgI+ANoAJgCyAAIAECICe0wA2ADcACgYPBhcAPKcCOQI6AjsCPAI9Aj4CP4BNgE6AT4BQgFGAUoBTpwYYBhkGGgYbBhwGHQYegJ+AoIChgKKApIClgKaAJd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwVkAFgAWABYAC0AWACgAjkAWABYABMAWIAAgACAAICdCAgICIAagE0ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAPoAEwVkAFgAWABYAC0AWACgAjoAWABYABMAWIAAgCKAAICdCAgICIAagE4ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwVkAFgAWABYAC0AWACgAjsAWABYABMAWIAAgACAAICdCAgICIAagE8ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATBk8AEwVkAFgAWABYAC0AWACgAjwAWABYABMAWIAAgKOAAICdCAgICIAagFAICIAACBECvN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwVkAFgAWABYAC0AWACgAj0AWABYABMAWIAAgACAAICdCAgICIAagFEICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwVkAFgAWABYAC0AWACgAj4AWABYABMAWIAAgACAAICdCAgICIAagFIICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwVkAFgAWABYAC0AWACgAj8AWABYABMAWIAAgACAAICdCAgICIAagFMICIAACFpkdXBsaWNhdGVz0gA3AAoGjACooIAZ0gCqAKsGjwaQWlhEUE1FbnRpdHmnBpEGkgaTBpQGlQaWAK9aWERQTUVudGl0eV1YRFVNTENsYXNzSW1wXxASWERVTUxDbGFzc2lmaWVySW1wXxARWERVTUxOYW1lc3BhY2VJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcNMANgA3AAoGmAaZADygoIAl0wA2ADcACgacBp0APKCggCXTADYANwAKBqAGoQA8oKCAJdIAqgCrBqQGpV5YRE1vZGVsUGFja2FnZaYGpganBqgGqQaqAK9eWERNb2RlbFBhY2thZ2VfEA9YRFVNTFBhY2thZ2VJbXBfEBFYRFVNTE5hbWVzcGFjZUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w0gA3AAoGrACooIAZ0wA2ADcACgavBrAAPKCggCVQ0gCqAKsGtAa1WVhEUE1Nb2RlbKMGtAa2AK9XWERNb2RlbF8QD05TS2V5ZWRBcmNoaXZlctEGuQAoVHJvb3SAAQAIABkAIgArADUAOgA/AaYBrAHJAdsB4gHvAgICGgIoAkICRAJGAkgCSgJMAk4CUAKJAqgCxQLkAvYDFgMdAzsDRwNjA2kDiwOsA78DwQPDA8UDxwPJA8sDzQPPA9ED0wPVA9cD2QPbA9wD4APtA/UEAAQDBAUECAQKBAwEFARXBHsEnwTCBOkFCQUwBVcFdwWbBb8FywXNBc8F0QXTBdUF1wXZBdsF3QXfBeEF4wXlBecF6AXtBfUGAgYFBgcGCgYMBg4GHQZCBmYGjQaxBrMGtQa3BrkGuwa9Br4GwAbNBuAG4gbkBuYG6AbqBuwG7gbwBvIHBQcHBwkHCwcNBw8HEQcTBxUHFwcZBy8HQgdeB3sHlwerB70H0wfsCCsIMQg6CEcIUwhdCGcIcgh9CIoIkgiUCJYImAiaCJsInAidCJ4IoAiiCKMIpAimCKcIsAixCLMIvAjHCNAI3wjmCO4I9wkACRMJHAkvCUYJWAmXCZkJmwmdCZ8JoAmhCaIJowmlCacJqAmpCasJrAnrCe0J7wnxCfMJ9An1CfYJ9wn5CfsJ/An9Cf8KAAoJCgoKDApLCk0KTwpRClMKVApVClYKVwpZClsKXApdCl8KYAqfCqEKowqlCqcKqAqpCqoKqwqtCq8KsAqxCrMKtAq9Cr4KwAr/CwELAwsFCwcLCAsJCwoLCwsNCw8LEAsRCxMLFAsVC1QLVgtYC1oLXAtdC14LXwtgC2ILZAtlC2YLaAtpC3YLdwt4C3oLgwuZC6ALrQvsC+4L8AvyC/QL9Qv2C/cL+Av6C/wL/Qv+DAAMAQwaDBwMHgwgDCEMIww6DEMMUQxeDGwMgQyVDKwMvgz9DP8NAQ0DDQUNBg0HDQgNCQ0LDQ0NDg0PDRENEg0kDS0NQg1RDWYNdA2JDZ0NtA3GDdMN3A3eDeAN4g3kDe0N7w3xDfMN9Q33DgQOCQ4MDhYOYQ6EDqQOxA7GDsgOyg7MDs4Ozw7QDtIO0w7VDtYO2A7aDtsO3A7eDt8O5A7xDvYO+A76Dv8PAQ8DDwUPGg8vD1QPeA+fD8MPxQ/HD8kPyw/ND88P0A/SD98P8A/yD/QP9g/4D/oP/A/+EAAQERATEBUQFxAZEBsQHRAfECEQIxBBEF8QchCGEJsQuBDMEOIRIREjESURJxEpESoRKxEsES0RLxExETIRMxE1ETYRdRF3EXkRexF9EX4RfxGAEYERgxGFEYYRhxGJEYoRyRHLEc0RzxHREdIR0xHUEdUR1xHZEdoR2xHdEd4R6xHsEe0R7xIuEjASMhI0EjYSNxI4EjkSOhI8Ej4SPxJAEkISQxKCEoQShhKIEooSixKMEo0SjhKQEpISkxKUEpYSlxKYEtcS2RLbEt0S3xLgEuES4hLjEuUS5xLoEukS6xLsEysTLRMvEzETMxM0EzUTNhM3EzkTOxM8Ez0TPxNAE38TgRODE4UThxOIE4kTihOLE40TjxOQE5ETkxOUE7kT3RQEFCgUKhQsFC4UMBQyFDQUNRQ3FEQUUxRVFFcUWRRbFF0UXxRhFHAUchR0FHYUeBR6FHwUfhSAFKAUyxTlFP4VGBU4FVsVmhWcFZ4VoBWiFaMVpBWlFaYVqBWqFasVrBWuFa8VsxXyFfQV9hX4FfoV+xX8Ff0V/hYAFgIWAxYEFgYWBxZGFkgWShZMFk4WTxZQFlEWUhZUFlYWVxZYFloWWxaaFpwWnhagFqIWoxakFqUWphaoFqoWqxasFq4WrxayFvEW8xb1FvcW+Rb6FvsW/Bb9Fv8XARcCFwMXBRcGF0UXRxdJF0sXTRdOF08XUBdRF1MXVRdWF1cXWRdaF5kXmxedF58XoReiF6MXpBelF6cXqReqF6sXrReuF7cXxRfSF+AX7RgAGBcYKRh0GJcYtxjXGNkY2xjdGN8Y4RjiGOMY5RjmGOgY6RjrGO0Y7hjvGPEY8hj3GQQZCRkLGQ0ZEhkUGRYZGBk9GWEZiBmsGa4ZsBmyGbQZthm4GbkZuxnIGdkZ2xndGd8Z4RnjGeUZ5xnpGfoZ/Bn+GgAaAhoEGgYaCBoKGgwaSxpNGk8aURpTGlQaVRpWGlcaWRpbGlwaXRpfGmAanxqhGqMapRqnGqgaqRqqGqsarRqvGrAasRqzGrQa8xr1Gvca+Rr7Gvwa/Rr+Gv8bARsDGwQbBRsHGwgbFRsWGxcbGRtYG1obXBteG2AbYRtiG2MbZBtmG2gbaRtqG2wbbRusG64bsBuyG7QbtRu2G7cbuBu6G7wbvRu+G8AbwRwAHAIcBBwGHAgcCRwKHAscDBwOHBAcERwSHBQcFRxUHFYcWBxaHFwcXRxeHF8cYBxiHGQcZRxmHGgcaRyoHKocrByuHLAcsRyyHLMctBy2HLgcuRy6HLwcvRziHQYdLR1RHVMdVR1XHVkdWx1dHV4dYB1tHXwdfh2AHYIdhB2GHYgdih2ZHZsdnR2fHaEdox2lHacdqR3oHeod7B3uHfAd8R3yHfMd9B32Hfgd+R36Hfwd/R48Hj4eQB5CHkQeRR5GHkceSB5KHkweTR5OHlAeUR6QHpIelB6WHpgemR6aHpsenB6eHqAeoR6iHqQepR7kHuYe6B7qHuwe7R7uHu8e8B7yHvQe9R72Hvge+R78HzsfPR8/H0EfQx9EH0UfRh9HH0kfSx9MH00fTx9QH48fkR+TH5Uflx+YH5kfmh+bH50fnx+gH6Efox+kH+Mf5R/nH+kf6x/sH+0f7h/vH/Ef8x/0H/Uf9x/4IEMgZiCGIKYgqCCqIKwgriCwILEgsiC0ILUgtyC4ILogvCC9IL4gwCDBIMYg0yDYINog3CDhIOMg5SDnIQwhMCFXIXshfSF/IYEhgyGFIYchiCGKIZchqCGqIawhriGwIbIhtCG2IbghySHLIc0hzyHRIdMh1SHXIdkh2yIaIhwiHiIgIiIiIyIkIiUiJiIoIioiKyIsIi4iLyJuInAiciJ0InYidyJ4InkieiJ8In4ifyKAIoIigyLCIsQixiLIIsoiyyLMIs0iziLQItIi0yLUItYi1yLkIuUi5iLoIycjKSMrIy0jLyMwIzEjMiMzIzUjNyM4IzkjOyM8I3sjfSN/I4EjgyOEI4UjhiOHI4kjiyOMI40jjyOQI88j0SPTI9Uj1yPYI9kj2iPbI90j3yPgI+Ej4yPkJCMkJSQnJCkkKyQsJC0kLiQvJDEkMyQ0JDUkNyQ4JHckeSR7JH0kfySAJIEkgiSDJIUkhySIJIkkiySMJLEk1ST8JSAlIiUkJSYlKCUqJSwlLSUvJTwlSyVNJU8lUSVTJVUlVyVZJWglaiVsJW4lcCVyJXQldiV4JbcluSW7Jb0lvyXAJcElwiXDJcUlxyXIJcklyyXMJc4mDSYPJhEmEyYVJhYmFyYYJhkmGyYdJh4mHyYhJiImYSZjJmUmZyZpJmomayZsJm0mbyZxJnImcyZ1JnYmtSa3Jrkmuya9Jr4mvybAJsEmwybFJsYmxybJJsomzScMJw4nECcSJxQnFScWJxcnGCcaJxwnHSceJyAnISdgJ2InZCdmJ2gnaSdqJ2snbCduJ3AncSdyJ3QndSe0J7YnuCe6J7wnvSe+J78nwCfCJ8QnxSfGJ8gnySgUKDcoVyh3KHkoeyh9KH8ogSiCKIMohSiGKIgoiSiLKI0ojiiPKJEokiiXKKQoqSirKK0osii0KLYouCjdKQEpKClMKU4pUClSKVQpVilYKVkpWyloKXkpeyl9KX8pgSmDKYUphymJKZopnCmeKaApoimkKaYpqCmqKawp6yntKe8p8SnzKfQp9Sn2Kfcp+Sn7Kfwp/Sn/KgAqPypBKkMqRSpHKkgqSSpKKksqTSpPKlAqUSpTKlQqkyqVKpcqmSqbKpwqnSqeKp8qoSqjKqQqpSqnKqgqtSq2KrcquSr4Kvoq/Cr+KwArASsCKwMrBCsGKwgrCSsKKwwrDStMK04rUCtSK1QrVStWK1crWCtaK1wrXSteK2ArYSugK6IrpCumK6grqSuqK6srrCuuK7ArsSuyK7QrtSv0K/Yr+Cv6K/wr/Sv+K/8sACwCLAQsBSwGLAgsCSxILEosTCxOLFAsUSxSLFMsVCxWLFgsWSxaLFwsXSyCLKYszSzxLPMs9Sz3LPks+yz9LP4tAC0NLRwtHi0gLSItJC0mLSgtKi05LTstPS0/LUEtQy1FLUctSS2ILYotjC2OLZAtkS2SLZMtlC2WLZgtmS2aLZwtnS3cLd4t4C3iLeQt5S3mLect6C3qLewt7S3uLfAt8S4wLjIuNC42LjguOS46LjsuPC4+LkAuQS5CLkQuRS6ELoYuiC6KLowujS6OLo8ukC6SLpQulS6WLpgumS6cLtsu3S7fLuEu4y7kLuUu5i7nLuku6y7sLu0u7y7wLy8vMS8zLzUvNy84LzkvOi87Lz0vPy9AL0EvQy9EL4MvhS+HL4kviy+ML40vji+PL5Evky+UL5Uvly+YL6MvrC+tL68vuC/DL9Iv3S/rMAAwFDArMD0wSjBLMEwwTjBbMFwwXTBfMGwwbTBuMHAweTCIMJUwpDC2MMow4TDzMPww/TD/MQwxDTEOMRAxETEaMSQxKzEzMUUxSjFPAAAAAAAAAgIAAAAAAAAGuwAAAAAAAAAAAAAAAAAAMVE= Resources/Data Models/HistoricLogFileStorageModel.xcdatamodeld/LogControllerStorageModel (model 3).xcdatamodel YnBsaXN0MDDUAAEAAgADAAQABQAGCWkJalgkdmVyc2lvblgkb2JqZWN0c1kkYXJjaGl2ZXJUJHRv cBIAAYagrxDuAAcACAAXADMANAA1AD0APgBZAFoAWwBhAGIAbgCEAIUAhgCHAIgAiQCKAIsAjACNAKYAqQCwALYAxQDUANcA5gD1APwBBQEGAQoBEAERARIBGAEZARoBIAEhASIAWAEyAUEBRQFJAVgBXgFfAWcBdgF3AYABkAGRAZIBkwGUAZUBlgGrAawBtAG1AbYBwgHWAdcB2AHZAdoB2wHcAd0B3gHtAfwCCwIPAh4CLQIuAj0CTAJbAmcCeQJ6AnsCfAJ9An4CfwKAAo8CngKtArwCvQLMAtsC6gLyAwcDCAMQAxwDMAM/A04DXQNhA3ADfwOOA50DrAO4A8oD2QPaA+kD+AQHBAgEFwQmBDUESgRLBFMEXwRzBIIEkQSgBKQEswTCBNEE4ATvBPsFDQUcBR0FLAU7BUoFSwVaBWkFeAWNBY4FlgWiBbYFxQXUBeMF5wX2BgUGFAYjBjIGPgZQBl8GbgZ9BowGmwaqBrkGzgbPBtcG4wb3BwYHFQckBygHNwdGB1UHZAdzB38HkQegB68HvgfNB84H3QfsB/sIEAgRCBkIJQg5CEgIVwhmCGoIeQiICJcIpgi1CMEI0wjiCPEJAAkPCR4JLQk8CT0JQAlJCU0JUQlVCV0JYAlkCWVVJG51bGzXAAkACgALAAwADQAOAA8AEAARABIAEwAUABMAFl8QD194ZF9yb290UGFja2FnZVYkY2xhc3NcX3hkX2NvbW1lbnRzXxAQX3hkX21vZGVsTWFuYWdlcl8QFV9jb25maWd1cmF0aW9uc0J5TmFtZV1feGRfbW9kZWxOYW1lXxAXX21vZGVsVmVyc2lvbklkZW50aWZpZXKAAoDtgOqAAIDrgACA7N4AGAAZABoAGwAcAB0AHgAKAB8AIAAhACIAIwAkACUAJgAnACgAJQATACsALAAtAC4ALwAlACUAE18QHFhEQnVja2V0Rm9yQ2xhc3Nlc3dhc0VuY29kZWRfEBpYREJ1Y2tldEZvclBhY2thZ2Vzc3RvcmFnZV8QHFhEQnVja2V0Rm9ySW50ZXJmYWNlc3N0b3JhZ2VfEA9feGRfb3duaW5nTW9kZWxfEB1YREJ1Y2tldEZvclBhY2thZ2Vzd2FzRW5jb2RlZFZfb3duZXJfEBtYREJ1Y2tldEZvckRhdGFUeXBlc3N0b3JhZ2VbX3Zpc2liaWxpdHlfEBlYREJ1Y2tldEZvckNsYXNzZXNzdG9yYWdlVV9uYW1lXxAfWERCdWNrZXRGb3JJbnRlcmZhY2Vzd2FzRW5jb2RlZF8QHlhEQnVja2V0Rm9yRGF0YVR5cGVzd2FzRW5jb2RlZF8QEF91bmlxdWVFbGVtZW50SUSABIDogOaAAYAEgACA54DpEACABYADgASABIAAUFNZRVPTADYANwAKADgAOgA8V05TLmtleXNaTlMub2JqZWN0c6EAOYAGoQA7gAeAMVhMb2dMaW5lMt8QEAA/AEAAQQBCAB0AQwBEAB8ARQBGAAoAIQBHAEgAJABJAEoASwAlACUAEABPAFAALQAlAEoAUwA5AEoAVgBXAFhfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2VfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNkdXBsaWNhdGVzXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3N0b3JhZ2VbX2lzQWJzdHJhY3SACYA5gASABIACgAqA44AEgAmA5YAGgAmA5IAICBMAAAABHk5WSVdvcmRlcmVk0wA2ADcACgBcAF4APKEAXYALoQBfgAyAMV5YRF9QU3RlcmVvdHlwZdkAHQAhAGMACgAkAGQAHwBJAGUAOwBdAEoAaQATACUALQBYAG1fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAB4ALgAmAOIAAgAQIgA3TADYANwAKAG8AeQA8qQBwAHEAcgBzAHQAdQB2AHcAeIAOgA+AEIARgBKAE4AUgBWAFqkAegB7AHwAfQB+AH8AgACBAIKAF4AbgByAHoAfgC2AL4AygDaAMV8QE1hEUE1Db21wb3VuZEluZGV4ZXNfEBBYRF9QU0tfZWxlbWVudElEXxAZWERQTVVuaXF1ZW5lc3NDb25zdHJhaW50c18QGlhEX1BTS192ZXJzaW9uSGFzaE1vZGlmaWVyXxAZWERfUFNLX2ZldGNoUmVxdWVzdHNBcnJheV8QEVhEX1BTS19pc0Fic3RyYWN0XxAPWERfUFNLX3VzZXJJbmZvXxATWERfUFNLX2NsYXNzTWFwcGluZ18QFlhEX1BTS19lbnRpdHlDbGFzc05hbWXfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwCZABMAXwBYAFgAWAAtAFgAoABwAFgAWAATAFhVX3R5cGVYX2RlZmF1bHRcX2Fzc29jaWF0aW9uW19pc1JlYWRPbmx5WV9pc1N0YXRpY1lfaXNVbmlxdWVaX2lzRGVyaXZlZFpfaXNPcmRlcmVkXF9pc0NvbXBvc2l0ZVdfaXNMZWFmgACAGIAAgAwICAgIgBqADggIgAAI0gA3AAoApwCooIAZ0gCqAKsArACtWiRjbGFzc25hbWVYJGNsYXNzZXNeTlNNdXRhYmxlQXJyYXmjAKwArgCvV05TQXJyYXlYTlNPYmplY3TSAKoAqwCxALJfEBBYRFVNTFByb3BlcnR5SW1wpACzALQAtQCvXxAQWERVTUxQcm9wZXJ0eUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAF8AWABYAFgALQBYAKAAcQBYAFgAEwBYgACAAIAAgAwICAgIgBqADwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAxwATAF8AWABYAFgALQBYAKAAcgBYAFgAEwBYgACAHYAAgAwICAgIgBqAEAgIgAAI0gA3AAoA1QCooIAZ3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAF8AWABYAFgALQBYAKAAcwBYAFgAEwBYgACAAIAAgAwICAgIgBqAEQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMA6AATAF8AWABYAFgALQBYAKAAdABYAFgAEwBYgACAIIAAgAwICAgIgBqAEggIgAAI0gA3AAoA9gCopAD3APgA+QD6gCGAJIAngCqAGdUAIQD9AP4A/wAKAQAAEwATADsBBF8QEF9mZXRjaGVkUHJvcGVydHlfEBBfcHJlZGljYXRlU3RyaW5nV19lbnRpdHmAIoAAgACAB4AjWkdlbmVyaWNBbGzSAKoAqwEHAQhfEBBYRFBNRmV0Y2hSZXF1ZXN0ogEJAK9fEBBYRFBNRmV0Y2hSZXF1ZXN01QAhAP0A/gD/AAoBCwATAQ0AOwEEgCWAAIAmgAeAI18QEkdlbmVyaWNDb25kaXRpb25hbF8QmWxvZ0xpbmVWaWV3SWRlbnRpZmllciA9PSAkdmlld19pZCBBTkQgZW50cnlJZGVudGlmaWVyID49ICRlbnRyeV9pZF9sb3dlc3QgQU5EIGVudHJ5SWRlbnRpZmllciA8PSAkZW50cnlfaWRfaGlnaGVzdCBBTkQgZW50cnlDcmVhdGlvbkRhdGUgPCAkY3JlYXRpb25fZGF0ZdUAIQD9AP4A/wAKARMAEwEVADsBBIAogACAKYAHgCNYVHJ1bmNhdGVfEElsb2dMaW5lVmlld0lkZW50aWZpZXIgPT0gJHZpZXdfaWQgQU5EIGVudHJ5SWRlbnRpZmllciA8PSAkZW50cnlfaWRfbG93ZXN01QAhAP0A/gD/AAoBGwATAR0AOwEEgCuAAIAsgAeAI18QEVVuaXF1ZUlkVG9FbnRyeUlkXxBLbG9nTGluZVZpZXdJZGVudGlmaWVyID09ICR2aWV3X2lkIEFORCBsb2dMaW5lVW5pcXVlSWRlbnRpZmllciA9PSAkdW5pcXVlX2lk3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATAF8AWABYAFgALQBYAKAAdQBYAFgAEwBYgACALoAAgAwICAgIgBqAEwgIgAAICN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATATQAEwBfAFgAWABYAC0AWACgAHYAWABYABMAWIAAgDCAAIAMCAgICIAagBQICIAACNMANgA3AAoBQgFDADygoIAx0gCqAKsBRgFHXxATTlNNdXRhYmxlRGljdGlvbmFyeaMBRgFIAK9cTlNEaWN0aW9uYXJ53xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBSwATAF8AWABYAFgALQBYAKAAdwBYAFgAEwBYgACAM4AAgAwICAgIgBqAFQgIgAAI1gAhAAoAJABJAB0AHwFZAVoAEwBYABMALYA0gDWAAAiAAF8QFFhER2VuZXJpY1JlY29yZENsYXNz0gCqAKsBYAFhXVhEVU1MQ2xhc3NJbXCmAWIBYwFkAWUBZgCvXVhEVU1MQ2xhc3NJbXBfEBJYRFVNTENsYXNzaWZpZXJJbXBfEBFYRFVNTE5hbWVzcGFjZUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBaQATAF8AWABYAFgALQBYAKAAeABYAFgAEwBYgACAN4AAgAwICAgIgBqAFggIgAAIXxAPTlNNYW5hZ2VkT2JqZWN00gCqAKsBeAF5XxASWERVTUxTdGVyZW90eXBlSW1wpwF6AXsBfAF9AX4BfwCvXxASWERVTUxTdGVyZW90eXBlSW1wXVhEVU1MQ2xhc3NJbXBfEBJYRFVNTENsYXNzaWZpZXJJbXBfEBFYRFVNTE5hbWVzcGFjZUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w0wA2ADcACgGBAYgAPKYBggGDAYQBhQGGAYeAOoA7gDyAPYA+gD+mAYkBigGLAYwBjQGOgECAa4CEgJ2AtIDMgDFfEBVsb2dMaW5lVmlld0lkZW50aWZpZXJfEBFlbnRyeUNyZWF0aW9uRGF0ZV8QD2VudHJ5SWRlbnRpZmllcl8QF2xvZ0xpbmVVbmlxdWVJZGVudGlmaWVyW2xvZ0xpbmVEYXRhXxARc2Vzc2lvbklkZW50aWZpZXLfEBIAjgCPAJABlwAdAJIAkwGYAB8AkQGZAJQACgAhAJUAlgAkAJcAEwATABMAJQA7AFgAWAGhAC0AWABKAFgBpQGCAFgAWAGpAFhfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIAHCAiAQgiACQiAaoA6CAiAQQgS489V8NMANgA3AAoBrQGwADyiAa4Br4BDgESiAbEBsoBFgFmAMV8QElhEX1BQcm9wU3RlcmVvdHlwZV8QElhEX1BBdHRfU3RlcmVvdHlwZdkAHQAhAbcACgAkAbgAHwBJAbkBiQGuAEoAaQATACUALQBYAcFfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAQIBDgAmAOIAAgAQIgEbTADYANwAKAcMBzAA8qAHEAcUBxgHHAcgByQHKAcuAR4BIgEmASoBLgEyATYBOqAHNAc4BzwHQAdEB0gHTAdSAT4BQgFGAU4BUgFaAV4BYgDFfEBtYRF9QUFNLX2lzU3RvcmVkSW5UcnV0aEZpbGVfEBtYRF9QUFNLX3ZlcnNpb25IYXNoTW9kaWZpZXJfEBBYRF9QUFNLX3VzZXJJbmZvXxARWERfUFBTS19pc0luZGV4ZWRfEBJYRF9QUFNLX2lzT3B0aW9uYWxfEBpYRF9QUFNLX2lzU3BvdGxpZ2h0SW5kZXhlZF8QEVhEX1BQU0tfZWxlbWVudElEXxATWERfUFBTS19pc1RyYW5zaWVudN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwGxAFgAWABYAC0AWACgAcQAWABYABMAWIAAgC6AAIBFCAgICIAagEcICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwGxAFgAWABYAC0AWACgAcUAWABYABMAWIAAgACAAIBFCAgICIAagEgICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAf4AEwGxAFgAWABYAC0AWACgAcYAWABYABMAWIAAgFKAAIBFCAgICIAagEkICIAACNMANgA3AAoCDAINADygoIAx3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATAbEAWABYAFgALQBYAKABxwBYAFgAEwBYgACALoAAgEUICAgIgBqASggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMCIAATAbEAWABYAFgALQBYAKAByABYAFgAEwBYgACAVYAAgEUICAgIgBqASwgIgAAICd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwGxAFgAWABYAC0AWACgAckAWABYABMAWIAAgC6AAIBFCAgICIAagEwICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwGxAFgAWABYAC0AWACgAcoAWABYABMAWIAAgACAAIBFCAgICIAagE0ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwGxAFgAWABYAC0AWACgAcsAWABYABMAWIAAgC6AAIBFCAgICIAagE4ICIAACNkAHQAhAlwACgAkAl0AHwBJAl4BiQGvAEoAaQATACUALQBYAmZfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAQIBEgAmAOIAAgAQIgFrTADYANwAKAmgCcAA8pwJpAmoCawJsAm0CbgJvgFuAXIBdgF6AX4BggGGnAnECcgJzAnQCdQJ2AneAYoBjgGSAZYBngGiAaYAxXxAdWERfUEF0dEtfZGVmYXVsdFZhbHVlQXNTdHJpbmdfEChYRF9QQXR0S19hbGxvd3NFeHRlcm5hbEJpbmFyeURhdGFTdG9yYWdlXxAXWERfUEF0dEtfbWluVmFsdWVTdHJpbmdfEBZYRF9QQXR0S19hdHRyaWJ1dGVUeXBlXxAXWERfUEF0dEtfbWF4VmFsdWVTdHJpbmdfEB1YRF9QQXR0S192YWx1ZVRyYW5zZm9ybWVyTmFtZV8QIFhEX1BBdHRLX3JlZ3VsYXJFeHByZXNzaW9uU3RyaW5n3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAbIAWABYAFgALQBYAKACaQBYAFgAEwBYgACAAIAAgFkICAgIgBqAWwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATAbIAWABYAFgALQBYAKACagBYAFgAEwBYgACALoAAgFkICAgIgBqAXAgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAbIAWABYAFgALQBYAKACawBYAFgAEwBYgACAAIAAgFkICAgIgBqAXQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMCrwATAbIAWABYAFgALQBYAKACbABYAFgAEwBYgACAZoAAgFkICAgIgBqAXggIgAAIEQK83xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAbIAWABYAFgALQBYAKACbQBYAFgAEwBYgACAAIAAgFkICAgIgBqAXwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAbIAWABYAFgALQBYAKACbgBYAFgAEwBYgACAAIAAgFkICAgIgBqAYAgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAbIAWABYAFgALQBYAKACbwBYAFgAEwBYgACAAIAAgFkICAgIgBqAYQgIgAAI0gCqAKsC6wLsXVhEUE1BdHRyaWJ1dGWmAu0C7gLvAvAC8QCvXVhEUE1BdHRyaWJ1dGVcWERQTVByb3BlcnR5XxAQWERVTUxQcm9wZXJ0eUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w3xASAI4AjwCQAvMAHQCSAJMC9AAfAJEC9QCUAAoAIQCVAJYAJACXABMAEwATACUAOwBYAFgC/QAtAFgASgBYAaUBgwBYAFgDBQBYXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASABwgIgG0IgAkIgGqAOwgIgGwIEjigAOrTADYANwAKAwkDDAA8ogGuAa+AQ4BEogMNAw6AboB5gDHZAB0AIQMRAAoAJAMSAB8ASQMTAYoBrgBKAGkAEwAlAC0AWAMbXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgGuAQ4AJgDiAAIAECIBv0wA2ADcACgMdAyYAPKgBxAHFAcYBxwHIAckBygHLgEeASIBJgEqAS4BMgE2ATqgDJwMoAykDKgMrAywDLQMugHCAcYBygHSAdYB2gHeAeIAx3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATAw0AWABYAFgALQBYAKABxABYAFgAEwBYgACALoAAgG4ICAgIgBqARwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATAw0AWABYAFgALQBYAKABxQBYAFgAEwBYgACAAIAAgG4ICAgIgBqASAgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMDUAATAw0AWABYAFgALQBYAKABxgBYAFgAEwBYgACAc4AAgG4ICAgIgBqASQgIgAAI0wA2ADcACgNeA18APKCggDHfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMDDQBYAFgAWAAtAFgAoAHHAFgAWAATAFiAAIAugACAbggICAiAGoBKCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwIgABMDDQBYAFgAWAAtAFgAoAHIAFgAWAATAFiAAIBVgACAbggICAiAGoBLCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMDDQBYAFgAWAAtAFgAoAHJAFgAWAATAFiAAIAugACAbggICAiAGoBMCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMDDQBYAFgAWAAtAFgAoAHKAFgAWAATAFiAAIAAgACAbggICAiAGoBNCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMDDQBYAFgAWAAtAFgAoAHLAFgAWAATAFiAAIAugACAbggICAiAGoBOCAiAAAjZAB0AIQOtAAoAJAOuAB8ASQOvAYoBrwBKAGkAEwAlAC0AWAO3XxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgGuARIAJgDiAAIAECIB60wA2ADcACgO5A8EAPKcCaQJqAmsCbAJtAm4Cb4BbgFyAXYBegF+AYIBhpwPCA8MDxAPFA8YDxwPIgHuAfYB+gH+AgYCCgIOAMd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATA8wAEwMOAFgAWABYAC0AWACgAmkAWABYABMAWIAAgHyAAIB5CAgICIAagFsICIAACFMwLjDfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMDDgBYAFgAWAAtAFgAoAJqAFgAWAATAFiAAIAugACAeQgICAiAGoBcCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMDDgBYAFgAWAAtAFgAoAJrAFgAWAATAFiAAIAAgACAeQgICAiAGoBdCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwP6ABMDDgBYAFgAWAAtAFgAoAJsAFgAWAATAFiAAICAgACAeQgICAiAGoBeCAiAAAgRAfTfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMDDgBYAFgAWAAtAFgAoAJtAFgAWAATAFiAAIAAgACAeQgICAiAGoBfCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMDDgBYAFgAWAAtAFgAoAJuAFgAWAATAFiAAIAAgACAeQgICAiAGoBgCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMDDgBYAFgAWAAtAFgAoAJvAFgAWAATAFiAAIAAgACAeQgICAiAGoBhCAiAAAjfEBIAjgCPAJAENgAdAJIAkwQ3AB8AkQQ4AJQACgAhAJUAlgAkAJcAEwATABMAJQA7AFgAWARAAC0AWABKAFgBpQGEAFgAWARIAFhfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIAHCAiAhgiACQiAaoA8CAiAhQgS9yHsndMANgA3AAoETARPADyiAa4Br4BDgESiBFAEUYCHgJKAMdkAHQAhBFQACgAkBFUAHwBJBFYBiwGuAEoAaQATACUALQBYBF5fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAhIBDgAmAOIAAgAQIgIjTADYANwAKBGAEaQA8qAHEAcUBxgHHAcgByQHKAcuAR4BIgEmASoBLgEyATYBOqARqBGsEbARtBG4EbwRwBHGAiYCKgIuAjYCOgI+AkICRgDHfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMEUABYAFgAWAAtAFgAoAHEAFgAWAATAFiAAIAugACAhwgICAiAGoBHCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMEUABYAFgAWAAtAFgAoAHFAFgAWAATAFiAAIAAgACAhwgICAiAGoBICAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwSTABMEUABYAFgAWAAtAFgAoAHGAFgAWAATAFiAAICMgACAhwgICAiAGoBJCAiAAAjTADYANwAKBKEEogA8oKCAMd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwRQAFgAWABYAC0AWACgAccAWABYABMAWIAAgC6AAICHCAgICIAagEoICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAiAAEwRQAFgAWABYAC0AWACgAcgAWABYABMAWIAAgFWAAICHCAgICIAagEsICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwRQAFgAWABYAC0AWACgAckAWABYABMAWIAAgC6AAICHCAgICIAagEwICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwRQAFgAWABYAC0AWACgAcoAWABYABMAWIAAgACAAICHCAgICIAagE0ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwRQAFgAWABYAC0AWACgAcsAWABYABMAWIAAgC6AAICHCAgICIAagE4ICIAACNkAHQAhBPAACgAkBPEAHwBJBPIBiwGvAEoAaQATACUALQBYBPpfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAhIBEgAmAOIAAgAQIgJPTADYANwAKBPwFBAA8pwJpAmoCawJsAm0CbgJvgFuAXIBdgF6AX4BggGGnBQUFBgUHBQgFCQUKBQuAlICWgJeAmICagJuAnIAx3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMFDwATBFEAWABYAFgALQBYAKACaQBYAFgAEwBYgACAlYAAgJIICAgIgBqAWwgIgAAIUTDfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMEUQBYAFgAWAAtAFgAoAJqAFgAWAATAFiAAIAugACAkggICAiAGoBcCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMEUQBYAFgAWAAtAFgAoAJrAFgAWAATAFiAAIAAgACAkggICAiAGoBdCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwU9ABMEUQBYAFgAWAAtAFgAoAJsAFgAWAATAFiAAICZgACAkggICAiAGoBeCAiAAAgRASzfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMEUQBYAFgAWAAtAFgAoAJtAFgAWAATAFiAAIAAgACAkggICAiAGoBfCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMEUQBYAFgAWAAtAFgAoAJuAFgAWAATAFiAAIAAgACAkggICAiAGoBgCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMEUQBYAFgAWAAtAFgAoAJvAFgAWAATAFiAAIAAgACAkggICAiAGoBhCAiAAAjfEBIAjgCPAJAFeQAdAJIAkwV6AB8AkQV7AJQACgAhAJUAlgAkAJcAEwATABMAJQA7AFgAWAWDAC0AWABKAFgBpQGFAFgAWAWLAFhfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIAHCAiAnwiACQiAaoA9CAiAnggSKZL7qdMANgA3AAoFjwWSADyiAa4Br4BDgESiBZMFlICggKuAMdkAHQAhBZcACgAkBZgAHwBJBZkBjAGuAEoAaQATACUALQBYBaFfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAnYBDgAmAOIAAgAQIgKHTADYANwAKBaMFrAA8qAHEAcUBxgHHAcgByQHKAcuAR4BIgEmASoBLgEyATYBOqAWtBa4FrwWwBbEFsgWzBbSAooCjgKSApoCngKiAqYCqgDHfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMFkwBYAFgAWAAtAFgAoAHEAFgAWAATAFiAAIAugACAoAgICAiAGoBHCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMFkwBYAFgAWAAtAFgAoAHFAFgAWAATAFiAAIAAgACAoAgICAiAGoBICAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwXWABMFkwBYAFgAWAAtAFgAoAHGAFgAWAATAFiAAIClgACAoAgICAiAGoBJCAiAAAjTADYANwAKBeQF5QA8oKCAMd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwWTAFgAWABYAC0AWACgAccAWABYABMAWIAAgC6AAICgCAgICIAagEoICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATAiAAEwWTAFgAWABYAC0AWACgAcgAWABYABMAWIAAgFWAAICgCAgICIAagEsICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwWTAFgAWABYAC0AWACgAckAWABYABMAWIAAgC6AAICgCAgICIAagEwICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwWTAFgAWABYAC0AWACgAcoAWABYABMAWIAAgACAAICgCAgICIAagE0ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwWTAFgAWABYAC0AWACgAcsAWABYABMAWIAAgC6AAICgCAgICIAagE4ICIAACNkAHQAhBjMACgAkBjQAHwBJBjUBjAGvAEoAaQATACUALQBYBj1fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAnYBEgAmAOIAAgAQIgKzTADYANwAKBj8GRwA8pwJpAmoCawJsAm0CbgJvgFuAXIBdgF6AX4BggGGnBkgGSQZKBksGTAZNBk6ArYCugK+AsICxgLKAs4Ax3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBZQAWABYAFgALQBYAKACaQBYAFgAEwBYgACAAIAAgKsICAgIgBqAWwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATBZQAWABYAFgALQBYAKACagBYAFgAEwBYgACALoAAgKsICAgIgBqAXAgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBZQAWABYAFgALQBYAKACawBYAFgAEwBYgACAAIAAgKsICAgIgBqAXQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMCrwATBZQAWABYAFgALQBYAKACbABYAFgAEwBYgACAZoAAgKsICAgIgBqAXggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBZQAWABYAFgALQBYAKACbQBYAFgAEwBYgACAAIAAgKsICAgIgBqAXwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBZQAWABYAFgALQBYAKACbgBYAFgAEwBYgACAAIAAgKsICAgIgBqAYAgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBZQAWABYAFgALQBYAKACbwBYAFgAEwBYgACAAIAAgKsICAgIgBqAYQgIgAAI3xASAI4AjwCQBroAHQCSAJMGuwAfAJEGvACUAAoAIQCVAJYAJACXABMAEwATACUAOwBYAFgGxAAtAFgASgBYAaUBhgBYAFgGzABYXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASABwgIgLYIgAkIgGqAPggIgLUIEqf4upDTADYANwAKBtAG0wA8ogGuAa+AQ4BEogbUBtWAt4DCgDHZAB0AIQbYAAoAJAbZAB8ASQbaAY0BrgBKAGkAEwAlAC0AWAbiXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgLSAQ4AJgDiAAIAECIC40wA2ADcACgbkBu0APKgBxAHFAcYBxwHIAckBygHLgEeASIBJgEqAS4BMgE2ATqgG7gbvBvAG8QbyBvMG9Ab1gLmAuoC7gL2AvoC/gMCAwYAx3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATBtQAWABYAFgALQBYAKABxABYAFgAEwBYgACALoAAgLcICAgIgBqARwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATBtQAWABYAFgALQBYAKABxQBYAFgAEwBYgACAAIAAgLcICAgIgBqASAgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMHFwATBtQAWABYAFgALQBYAKABxgBYAFgAEwBYgACAvIAAgLcICAgIgBqASQgIgAAI0wA2ADcACgclByYAPKCggDHfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMG1ABYAFgAWAAtAFgAoAHHAFgAWAATAFiAAIAugACAtwgICAiAGoBKCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwIgABMG1ABYAFgAWAAtAFgAoAHIAFgAWAATAFiAAIBVgACAtwgICAiAGoBLCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMG1ABYAFgAWAAtAFgAoAHJAFgAWAATAFiAAIAugACAtwgICAiAGoBMCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMG1ABYAFgAWAAtAFgAoAHKAFgAWAATAFiAAIAAgACAtwgICAiAGoBNCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMG1ABYAFgAWAAtAFgAoAHLAFgAWAATAFiAAIAugACAtwgICAiAGoBOCAiAAAjZAB0AIQd0AAoAJAd1AB8ASQd2AY0BrwBKAGkAEwAlAC0AWAd+XxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgLSARIAJgDiAAIAECIDD0wA2ADcACgeAB4gAPKcCaQJqAmsCbAJtAm4Cb4BbgFyAXYBegF+AYIBhpweJB4oHiweMB40HjgePgMSAxYDGgMeAyYDKgMuAMd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwbVAFgAWABYAC0AWACgAmkAWABYABMAWIAAgACAAIDCCAgICIAagFsICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwbVAFgAWABYAC0AWACgAmoAWABYABMAWIAAgC6AAIDCCAgICIAagFwICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwbVAFgAWABYAC0AWACgAmsAWABYABMAWIAAgACAAIDCCAgICIAagF0ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATB8AAEwbVAFgAWABYAC0AWACgAmwAWABYABMAWIAAgMiAAIDCCAgICIAagF4ICIAACBED6N8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwbVAFgAWABYAC0AWACgAm0AWABYABMAWIAAgACAAIDCCAgICIAagF8ICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwbVAFgAWABYAC0AWACgAm4AWABYABMAWIAAgACAAIDCCAgICIAagGAICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwbVAFgAWABYAC0AWACgAm8AWABYABMAWIAAgACAAIDCCAgICIAagGEICIAACN8QEgCOAI8AkAf8AB0AkgCTB/0AHwCRB/4AlAAKACEAlQCWACQAlwATABMAEwAlADsAWABYCAYALQBYAEoAWAGlAYcAWABYCA4AWF8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZIAAgACAAIAEgAcICIDOCIAJCIBqgD8ICIDNCBK09YDQ0wA2ADcACggSCBUAPKIBrgGvgEOARKIIFggXgM+A2oAx2QAdACEIGgAKACQIGwAfAEkIHAGOAa4ASgBpABMAJQAtAFgIJF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYDMgEOACYA4gACABAiA0NMANgA3AAoIJggvADyoAcQBxQHGAccByAHJAcoBy4BHgEiASYBKgEuATIBNgE6oCDAIMQgyCDMINAg1CDYIN4DRgNKA04DVgNaA14DYgNmAMd8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATASQAEwgWAFgAWABYAC0AWACgAcQAWABYABMAWIAAgC6AAIDPCAgICIAagEcICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATABMAEwgWAFgAWABYAC0AWACgAcUAWABYABMAWIAAgACAAIDPCAgICIAagEgICIAACN8QDwCOAI8AkAAdAJEAkgCTAB8AlAAKACEAlQCWACQAlwATCFkAEwgWAFgAWABYAC0AWACgAcYAWABYABMAWIAAgNSAAIDPCAgICIAagEkICIAACNMANgA3AAoIZwhoADygoIAx3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATCBYAWABYAFgALQBYAKABxwBYAFgAEwBYgACALoAAgM8ICAgIgBqASggIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATCBYAWABYAFgALQBYAKAByABYAFgAEwBYgACALoAAgM8ICAgIgBqASwgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATCBYAWABYAFgALQBYAKAByQBYAFgAEwBYgACALoAAgM8ICAgIgBqATAgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMAEwATCBYAWABYAFgALQBYAKABygBYAFgAEwBYgACAAIAAgM8ICAgIgBqATQgIgAAI3xAPAI4AjwCQAB0AkQCSAJMAHwCUAAoAIQCVAJYAJACXABMBJAATCBYAWABYAFgALQBYAKABywBYAFgAEwBYgACALoAAgM8ICAgIgBqATggIgAAI2QAdACEItgAKACQItwAfAEkIuAGOAa8ASgBpABMAJQAtAFgIwF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYDMgESACYA4gACABAiA29MANgA3AAoIwgjKADynAmkCagJrAmwCbQJuAm+AW4BcgF2AXoBfgGCAYacIywjMCM0IzgjPCNAI0YDcgN2A3oDfgOCA4YDigDHfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwUPABMIFwBYAFgAWAAtAFgAoAJpAFgAWAATAFiAAICVgACA2ggICAiAGoBbCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwEkABMIFwBYAFgAWAAtAFgAoAJqAFgAWAATAFiAAIAugACA2ggICAiAGoBcCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMIFwBYAFgAWAAtAFgAoAJrAFgAWAATAFiAAIAAgACA2ggICAiAGoBdCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwU9ABMIFwBYAFgAWAAtAFgAoAJsAFgAWAATAFiAAICZgACA2ggICAiAGoBeCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMIFwBYAFgAWAAtAFgAoAJtAFgAWAATAFiAAIAAgACA2ggICAiAGoBfCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMIFwBYAFgAWAAtAFgAoAJuAFgAWAATAFiAAIAAgACA2ggICAiAGoBgCAiAAAjfEA8AjgCPAJAAHQCRAJIAkwAfAJQACgAhAJUAlgAkAJcAEwATABMIFwBYAFgAWAAtAFgAoAJvAFgAWAATAFiAAIAAgACA2ggICAiAGoBhCAiAAAhaZHVwbGljYXRlc9IANwAKCT4AqKCAGdIAqgCrCUEJQlpYRFBNRW50aXR5pwlDCUQJRQlGCUcJSACvWlhEUE1FbnRpdHldWERVTUxDbGFzc0ltcF8QElhEVU1MQ2xhc3NpZmllckltcF8QEVhEVU1MTmFtZXNwYWNlSW1wXxAUWERVTUxOYW1lZEVsZW1lbnRJbXBfEA9YRFVNTEVsZW1lbnRJbXDTADYANwAKCUoJSwA8oKCAMdMANgA3AAoJTglPADygoIAx0wA2ADcACglSCVMAPKCggDHSAKoAqwlWCVdeWERNb2RlbFBhY2thZ2WmCVgJWQlaCVsJXACvXlhETW9kZWxQYWNrYWdlXxAPWERVTUxQYWNrYWdlSW1wXxARWERVTUxOYW1lc3BhY2VJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcNIANwAKCV4AqKCAGdMANgA3AAoJYQliADygoIAxUNIAqgCrCWYJZ1lYRFBNTW9kZWyjCWYJaACvV1hETW9kZWxfEA9OU0tleWVkQXJjaGl2ZXLRCWsAKFRyb290gAEACAAZACIAKwA1ADoAPwIeAiQCQQJTAloCZwJ6ApICoAK6ArwCvgLAAsICxALGAsgDAQMgAz0DXANuA44DlQOzA78D2wPhBAMEJAQ3BDkEOwQ9BD8EQQRDBEUERwRJBEsETQRPBFEEUwRUBFgEZQRtBHgEewR9BIAEggSEBI0E0AT0BRgFOwViBYIFqQXQBfAGFAY4BkQGRgZIBkoGTAZOBlAGUgZUBlYGWAZaBlwGXgZgBmEGagZyBn8GggaEBocGiQaLBpoGvwbjBwoHLgcwBzIHNAc2BzgHOgc7Bz0HSgddB18HYQdjB2UHZwdpB2sHbQdvB4IHhAeGB4gHigeMB44HkAeSB5QHlgesB78H2wf4CBQIKAg6CFAIaQioCK4ItwjECNAI2gjkCO8I+gkHCQ8JEQkTCRUJFwkYCRkJGgkbCR0JHwkgCSEJIwkkCS0JLgkwCTkJRAlNCVwJYwlrCXQJfQmQCZkJrAnDCdUKFAoWChgKGgocCh0KHgofCiAKIgokCiUKJgooCikKaApqCmwKbgpwCnEKcgpzCnQKdgp4CnkKegp8Cn0KhgqHCokKyArKCswKzgrQCtEK0grTCtQK1grYCtkK2grcCt0LHAseCyALIgskCyULJgsnCygLKgssCy0LLgswCzELOgtDC0ULRwtJC0sLTQtiC3ULiAuQC5ILlAuWC5gLmgulC64LwQvGC9kL7gvwC/IL9Av2C/gMDQypDL4MwAzCDMQMxgzIDNENHQ0yDTQNNg04DToNPA1QDZ4N3Q3fDeEN4w3lDeYN5w3oDekN6w3tDe4N7w3xDfIN8w4yDjQONg44DjoOOw48Dj0OPg5ADkIOQw5EDkYORw5UDlUOVg5YDmEOdw5+DosOyg7MDs4O0A7SDtMO1A7VDtYO2A7aDtsO3A7eDt8O+A76DvwO/g7/DwEPGA8hDy8PPA9KD18Pcw+KD5wP2w/dD98P4Q/jD+QP5Q/mD+cP6Q/rD+wP7Q/vD/AQAhALECAQLxBEEFIQZxB7EJIQpBCxEL4QwBDCEMQQxhDIEMoQ1xDZENsQ3RDfEOEQ4xDlEP0REREjET0RSRFdEagRyxHrEgsSDRIPEhESExIVEhYSFxIZEhoSHBIdEh8SIRIiEiMSJRImEisSOBI9Ej8SQRJGEkgSShJMEmESdhKbEr8S5hMKEwwTDhMQExITFBMWExcTGRMmEzcTORM7Ez0TPxNBE0MTRRNHE1gTWhNcE14TYBNiE2QTZhNoE2oTiBOmE7kTzRPiE/8UExQpFGgUahRsFG4UcBRxFHIUcxR0FHYUeBR5FHoUfBR9FLwUvhTAFMIUxBTFFMYUxxTIFMoUzBTNFM4U0BTRFRAVEhUUFRYVGBUZFRoVGxUcFR4VIBUhFSIVJBUlFTIVMxU0FTYVdRV3FXkVexV9FX4VfxWAFYEVgxWFFYYVhxWJFYoVyRXLFc0VzxXRFdIV0xXUFdUV1xXZFdoV2xXdFd4V3xYeFiAWIhYkFiYWJxYoFikWKhYsFi4WLxYwFjIWMxZyFnQWdhZ4FnoWexZ8Fn0WfhaAFoIWgxaEFoYWhxbGFsgWyhbMFs4WzxbQFtEW0hbUFtYW1xbYFtoW2xcAFyQXSxdvF3EXcxd1F3cXeRd7F3wXfheLF5oXnBeeF6AXohekF6YXqBe3F7kXuxe9F78XwRfDF8UXxxfnGBIYLBhFGF8YfxiiGOEY4xjlGOcY6RjqGOsY7BjtGO8Y8RjyGPMY9Rj2GTUZNxk5GTsZPRk+GT8ZQBlBGUMZRRlGGUcZSRlKGYkZixmNGY8ZkRmSGZMZlBmVGZcZmRmaGZsZnRmeGd0Z3xnhGeMZ5RnmGecZ6BnpGesZ7RnuGe8Z8RnyGfUaNBo2GjgaOho8Gj0aPho/GkAaQhpEGkUaRhpIGkkaiBqKGowajhqQGpEakhqTGpQalhqYGpkamhqcGp0a3BreGuAa4hrkGuUa5hrnGuga6hrsGu0a7hrwGvEa+hsIGxUbIxswG0MbWhtsG7cb2hv6HBocHBweHCAcIhwkHCUcJhwoHCkcKxwsHC4cMBwxHDIcNBw1HDocRxxMHE4cUBxVHFccWRxbHIAcpBzLHO8c8RzzHPUc9xz5HPsc/Bz+HQsdHB0eHSAdIh0kHSYdKB0qHSwdPR0/HUEdQx1FHUcdSR1LHU0dTx2OHZAdkh2UHZYdlx2YHZkdmh2cHZ4dnx2gHaIdox3iHeQd5h3oHeod6x3sHe0d7h3wHfId8x30HfYd9x42HjgeOh48Hj4ePx5AHkEeQh5EHkYeRx5IHkoeSx5YHlkeWh5cHpsenR6fHqEeox6kHqUeph6nHqkeqx6sHq0erx6wHu8e8R7zHvUe9x74Hvke+h77Hv0e/x8AHwEfAx8EH0MfRR9HH0kfSx9MH00fTh9PH1EfUx9UH1UfVx9YH5cfmR+bH50fnx+gH6Efoh+jH6Ufpx+oH6kfqx+sH+sf7R/vH/Ef8x/0H/Uf9h/3H/kf+x/8H/0f/yAAICUgSSBwIJQgliCYIJognCCeIKAgoSCjILAgvyDBIMMgxSDHIMkgyyDNINwg3iDgIOIg5CDmIOgg6iDsISshLSEvITEhMyE0ITUhNiE3ITkhOyE8IT0hPyFAIUQhgyGFIYchiSGLIYwhjSGOIY8hkSGTIZQhlSGXIZgh1yHZIdsh3SHfIeAh4SHiIeMh5SHnIegh6SHrIewiKyItIi8iMSIzIjQiNSI2IjciOSI7IjwiPSI/IkAiQyKCIoQihiKIIooiiyKMIo0ijiKQIpIikyKUIpYilyLWItgi2iLcIt4i3yLgIuEi4iLkIuYi5yLoIuoi6yMqIywjLiMwIzIjMyM0IzUjNiM4IzojOyM8Iz4jPyOKI60jzSPtI+8j8SPzI/Uj9yP4I/kj+yP8I/4j/yQBJAMkBCQFJAckCCQNJBokHyQhJCMkKCQqJCwkLiRTJHckniTCJMQkxiTIJMokzCTOJM8k0STeJO8k8STzJPUk9yT5JPsk/ST/JRAlEiUUJRYlGCUaJRwlHiUgJSIlYSVjJWUlZyVpJWolayVsJW0lbyVxJXIlcyV1JXYltSW3JbkluyW9Jb4lvyXAJcElwyXFJcYlxyXJJcomCSYLJg0mDyYRJhImEyYUJhUmFyYZJhomGyYdJh4mKyYsJi0mLyZuJnAmciZ0JnYmdyZ4JnkmeiZ8Jn4mfyaAJoImgybCJsQmxibIJsomyybMJs0mzibQJtIm0ybUJtYm1ycWJxgnGiccJx4nHycgJyEnIickJyYnJycoJyonKydqJ2wnbidwJ3Incyd0J3Undid4J3oneyd8J34nfye+J8AnwifEJ8YnxyfIJ8knyifMJ84nzyfQJ9In0yf4KBwoQyhnKGkoayhtKG8ocShzKHQodiiDKJIolCiWKJgomiicKJ4ooCivKLEosyi1KLcouSi7KL0ovyj+KQApAikEKQYpBykIKQkpCikMKQ4pDykQKRIpEykVKVQpVilYKVopXCldKV4pXylgKWIpZCllKWYpaClpKagpqimsKa4psCmxKbIpsym0KbYpuCm5KbopvCm9Kfwp/ioAKgIqBCoFKgYqByoIKgoqDCoNKg4qECoRKhQqUypVKlcqWSpbKlwqXSpeKl8qYSpjKmQqZSpnKmgqpyqpKqsqrSqvKrAqsSqyKrMqtSq3KrgquSq7Krwq+yr9Kv8rASsDKwQrBSsGKwcrCSsLKwwrDSsPKxArWyt+K54rvivAK8IrxCvGK8grySvKK8wrzSvPK9Ar0ivUK9Ur1ivYK9kr3ivrK/Ar8iv0K/kr+yv9K/8sJCxILG8skyyVLJcsmSybLJ0snyygLKIsryzALMIsxCzGLMgsyizMLM4s0CzhLOMs5SznLOks6yztLO8s8SzzLTItNC02LTgtOi07LTwtPS0+LUAtQi1DLUQtRi1HLYYtiC2KLYwtji2PLZAtkS2SLZQtli2XLZgtmi2bLdot3C3eLeAt4i3jLeQt5S3mLegt6i3rLewt7i3vLfwt/S3+LgAuPy5BLkMuRS5HLkguSS5KLksuTS5PLlAuUS5TLlQuky6VLpcumS6bLpwunS6eLp8uoS6jLqQupS6nLqgu5y7pLusu7S7vLvAu8S7yLvMu9S73Lvgu+S77LvwvOy89Lz8vQS9DL0QvRS9GL0cvSS9LL0wvTS9PL1Avjy+RL5MvlS+XL5gvmS+aL5svnS+fL6AvoS+jL6QvyS/tMBQwODA6MDwwPjBAMEIwRDBFMEcwVDBjMGUwZzBpMGswbTBvMHEwgDCCMIQwhjCIMIowjDCOMJAwzzDRMNMw1TDXMNgw2TDaMNsw3TDfMOAw4TDjMOQxIzElMScxKTErMSwxLTEuMS8xMTEzMTQxNTE3MTgxdzF5MXsxfTF/MYAxgTGCMYMxhTGHMYgxiTGLMYwxyzHNMc8x0THTMdQx1THWMdcx2THbMdwx3THfMeAyHzIhMiMyJTInMigyKTIqMisyLTIvMjAyMTIzMjQyczJ1MncyeTJ7MnwyfTJ+Mn8ygTKDMoQyhTKHMogyxzLJMssyzTLPMtAy0TLSMtMy1TLXMtgy2TLbMtwzJzNKM2ozijOMM44zkDOSM5QzlTOWM5gzmTObM5wznjOgM6EzojOkM6UzqjO3M7wzvjPAM8UzxzPJM8sz8DQUNDs0XzRhNGM0ZTRnNGk0azRsNG40ezSMNI40kDSSNJQ0ljSYNJo0nDStNK80sTSzNLU0tzS5NLs0vTS/NP41ADUCNQQ1BjUHNQg1CTUKNQw1DjUPNRA1EjUTNVI1VDVWNVg1WjVbNVw1XTVeNWA1YjVjNWQ1ZjVnNaY1qDWqNaw1rjWvNbA1sTWyNbQ1tjW3Nbg1ujW7Ncg1yTXKNcw2CzYNNg82ETYTNhQ2FTYWNhc2GTYbNhw2HTYfNiA2XzZhNmM2ZTZnNmg2aTZqNms2bTZvNnA2cTZzNnQ2sza1Nrc2uTa7Nrw2vTa+Nr82wTbDNsQ2xTbHNsg3BzcJNws3DTcPNxA3ETcSNxM3FTcXNxg3GTcbNxw3WzddN183YTdjN2Q3ZTdmN2c3aTdrN2w3bTdvN3A3lTe5N+A4BDgGOAg4CjgMOA44EDgROBM4IDgvODE4Mzg1ODc4OTg7OD04TDhOOFA4UjhUOFY4WDhaOFw4mzidOJ84oTijOKQ4pTimOKc4qTirOKw4rTivOLA47zjxOPM49Tj3OPg4+Tj6OPs4/Tj/OQA5ATkDOQQ5QzlFOUc5STlLOUw5TTlOOU85UTlTOVQ5VTlXOVg5lzmZOZs5nTmfOaA5oTmiOaM5pTmnOag5qTmrOaw5rznuOfA58jn0OfY59zn4Ofk5+jn8Of45/zoAOgI6AzpCOkQ6RjpIOko6SzpMOk06TjpQOlI6UzpUOlY6VzqWOpg6mjqcOp46nzqgOqE6ojqkOqY6pzqoOqo6qzr2Oxk7OTtZO1s7XTtfO2E7YztkO2U7ZztoO2o7azttO287cDtxO3M7dDt5O4Y7izuNO487lDuWO5g7mju/O+M8CjwuPDA8Mjw0PDY8ODw6PDs8PTxKPFs8XTxfPGE8YzxlPGc8aTxrPHw8fjyAPII8hDyGPIg8ijyMPI48zTzPPNE80zzVPNY81zzYPNk82zzdPN483zzhPOI9IT0jPSU9Jz0pPSo9Kz0sPS09Lz0xPTI9Mz01PTY9dT13PXk9ez19PX49fz2APYE9gz2FPYY9hz2JPYo9lz2YPZk9mz3aPdw93j3gPeI94z3kPeU95j3oPeo96z3sPe497z4uPjA+Mj40PjY+Nz44Pjk+Oj48Pj4+Pz5APkI+Qz6CPoQ+hj6IPoo+iz6MPo0+jj6QPpI+kz6UPpY+lz7WPtg+2j7cPt4+3z7gPuE+4j7kPuY+5z7oPuo+6z8qPyw/Lj8wPzI/Mz80PzU/Nj84Pzo/Oz88Pz4/Pz9kP4g/rz/TP9U/1z/ZP9s/3T/fP+A/4j/vP/5AAEACQARABkAIQApADEAbQB1AH0AhQCNAJUAnQClAK0BqQGxAbkBwQHJAc0B0QHVAdkB4QHpAe0B8QH5Af0C+QMBAwkDEQMZAx0DIQMlAykDMQM5Az0DQQNJA00ESQRRBFkEYQRpBG0EcQR1BHkEgQSJBI0EkQSZBJ0FmQWhBakFsQW5Bb0FwQXFBckF0QXZBd0F4QXpBe0G6QbxBvkHAQcJBw0HEQcVBxkHIQcpBy0HMQc5Bz0IOQhBCEkIUQhZCF0IYQhlCGkIcQh5CH0IgQiJCI0JiQmRCZkJoQmpCa0JsQm1CbkJwQnJCc0J0QnZCd0KCQotCjEKOQpdCokKxQrxCykLfQvNDCkMcQylDKkMrQy1DOkM7QzxDPkNLQ0xDTUNPQ1hDZ0N0Q4NDlUOpQ8BD0kPbQ9xD3kPrQ+xD7UPvQ/BD+UQDRApEEkQkRClELgAAAAAAAAICAAAAAAAACW0AAAAAAAAAAAAAAAAAAEQw YnBsaXN0MDDUAQIDBAUGODlYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKwH CBMUGRoiJywtMDRVJG51bGzVCQoLDA0ODxAREllOU09wZXJhbmReTlNTZWxlY3Rvck5hbWVfEBBOU0V4cHJlc3Npb25UeXBlW05TQXJndW1lbnRzViRjbGFzc4ADgAIQBIAGgAtfEBB2YWx1ZUZvcktleVBhdGg60xULDRYXGFpOU1ZhcmlhYmxlgAQQAoAFVnNvdXJjZdIbHB0eWiRjbGFzc25hbWVYJGNsYXNzZXNfEBROU1ZhcmlhYmxlRXhwcmVzc2lvbqMfICFfEBROU1ZhcmlhYmxlRXhwcmVzc2lvblxOU0V4cHJlc3Npb25YTlNPYmplY3TSIw0kJlpOUy5vYmplY3RzoSWAB4AK0w0LKCkqK1lOU0tleVBhdGiACRAKgAhcY3JlYXRpb25EYXRl0hscLi9fEBxOU0tleVBhdGhTcGVjaWZpZXJFeHByZXNzaW9uoy4gIdIbHDEyXk5TTXV0YWJsZUFycmF5ozEzIVdOU0FycmF50hscNTZfEBNOU0tleVBhdGhFeHByZXNzaW9upDU3ICFfEBROU0Z1bmN0aW9uRXhwcmVzc2lvbl8QD05TS2V5ZWRBcmNoaXZlctE6O1Ryb290gAEACAARABoAIwAtADIANwBEAEoAVQBfAG4AgQCNAJQAlgCYAJoAnACeALEAuADDAMUAxwDJANAA1QDgAOkBAAEEARsBKAExATYBQQFDAUUBRwFOAVgBWgFcAV4BawFwAY8BkwGYAacBqwGzAbgBzgHTAeoB/AH/AgQAAAAAAAACAQAAAAAAAAA8AAAAAAAAAAAAAAAAAAACBg== entryCreationDate YnBsaXN0MDDUAQIDBAUGODlYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKwH CBMUGRoiJywtMDRVJG51bGzVCQoLDA0ODxAREllOU09wZXJhbmReTlNTZWxlY3Rvck5hbWVfEBBOU0V4cHJlc3Npb25UeXBlW05TQXJndW1lbnRzViRjbGFzc4ADgAIQBIAGgAtfEBB2YWx1ZUZvcktleVBhdGg60xULDRYXGFpOU1ZhcmlhYmxlgAQQAoAFVnNvdXJjZdIbHB0eWiRjbGFzc25hbWVYJGNsYXNzZXNfEBROU1ZhcmlhYmxlRXhwcmVzc2lvbqMfICFfEBROU1ZhcmlhYmxlRXhwcmVzc2lvblxOU0V4cHJlc3Npb25YTlNPYmplY3TSIw0kJlpOUy5vYmplY3RzoSWAB4AK0w0LKCkqK1lOU0tleVBhdGiACRAKgAhUZGF0YdIbHC4vXxAcTlNLZXlQYXRoU3BlY2lmaWVyRXhwcmVzc2lvbqMuICHSGxwxMl5OU011dGFibGVBcnJheaMxMyFXTlNBcnJhedIbHDU2XxATTlNLZXlQYXRoRXhwcmVzc2lvbqQ1NyAhXxAUTlNGdW5jdGlvbkV4cHJlc3Npb25fEA9OU0tleWVkQXJjaGl2ZXLROjtUcm9vdIABAAgAEQAaACMALQAyADcARABKAFUAXwBuAIEAjQCUAJYAmACaAJwAngCxALgAwwDFAMcAyQDQANUA4ADpAQABBAEbASgBMQE2AUEBQwFFAUcBTgFYAVoBXAFeAWMBaAGHAYsBkAGfAaMBqwGwAcYBywHiAfQB9wH8AAAAAAAAAgEAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAf4= logLineData YnBsaXN0MDDUAQIDBAUGODlYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKwH CBMUGRoiJywtMDRVJG51bGzVCQoLDA0ODxAREllOU09wZXJhbmReTlNTZWxlY3Rvck5hbWVfEBBOU0V4cHJlc3Npb25UeXBlW05TQXJndW1lbnRzViRjbGFzc4ADgAIQBIAGgAtfEBB2YWx1ZUZvcktleVBhdGg60xULDRYXGFpOU1ZhcmlhYmxlgAQQAoAFVnNvdXJjZdIbHB0eWiRjbGFzc25hbWVYJGNsYXNzZXNfEBROU1ZhcmlhYmxlRXhwcmVzc2lvbqMfICFfEBROU1ZhcmlhYmxlRXhwcmVzc2lvblxOU0V4cHJlc3Npb25YTlNPYmplY3TSIw0kJlpOUy5vYmplY3RzoSWAB4AK0w0LKCkqK1lOU0tleVBhdGiACRAKgAhZY2hhbm5lbElk0hscLi9fEBxOU0tleVBhdGhTcGVjaWZpZXJFeHByZXNzaW9uoy4gIdIbHDEyXk5TTXV0YWJsZUFycmF5ozEzIVdOU0FycmF50hscNTZfEBNOU0tleVBhdGhFeHByZXNzaW9upDU3ICFfEBROU0Z1bmN0aW9uRXhwcmVzc2lvbl8QD05TS2V5ZWRBcmNoaXZlctE6O1Ryb290gAEACAARABoAIwAtADIANwBEAEoAVQBfAG4AgQCNAJQAlgCYAJoAnACeALEAuADDAMUAxwDJANAA1QDgAOkBAAEEARsBKAExATYBQQFDAUUBRwFOAVgBWgFcAV4BaAFtAYwBkAGVAaQBqAGwAbUBywHQAecB+QH8AgEAAAAAAAACAQAAAAAAAAA8AAAAAAAAAAAAAAAAAAACAw== logLineViewIdentifier HLSHistoricLogLineEntityMigration LogLine Undefined 1 LogLine2 1 YnBsaXN0MDDUAQIDBAUGODlYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKwH CBMUGRoiJywtMDRVJG51bGzVCQoLDA0ODxAREllOU09wZXJhbmReTlNTZWxlY3Rvck5hbWVfEBBOU0V4cHJlc3Npb25UeXBlW05TQXJndW1lbnRzViRjbGFzc4ADgAIQBIAGgAtfEBB2YWx1ZUZvcktleVBhdGg60xULDRYXGFpOU1ZhcmlhYmxlgAQQAoAFVnNvdXJjZdIbHB0eWiRjbGFzc25hbWVYJGNsYXNzZXNfEBROU1ZhcmlhYmxlRXhwcmVzc2lvbqMfICFfEBROU1ZhcmlhYmxlRXhwcmVzc2lvblxOU0V4cHJlc3Npb25YTlNPYmplY3TSIw0kJlpOUy5vYmplY3RzoSWAB4AK0w0LKCkqK1lOU0tleVBhdGiACRAKgAhSaWTSGxwuL18QHE5TS2V5UGF0aFNwZWNpZmllckV4cHJlc3Npb26jLiAh0hscMTJeTlNNdXRhYmxlQXJyYXmjMTMhV05TQXJyYXnSGxw1Nl8QE05TS2V5UGF0aEV4cHJlc3Npb26kNTcgIV8QFE5TRnVuY3Rpb25FeHByZXNzaW9uXxAPTlNLZXllZEFyY2hpdmVy0To7VHJvb3SAAQAIABEAGgAjAC0AMgA3AEQASgBVAF8AbgCBAI0AlACWAJgAmgCcAJ4AsQC4AMMAxQDHAMkA0ADVAOAA6QEAAQQBGwEoATEBNgFBAUMBRQFHAU4BWAFaAVwBXgFhAWYBhQGJAY4BnQGhAakBrgHEAckB4AHyAfUB+gAAAAAAAAIBAAAAAAAAADwAAAAAAAAAAAAAAAAAAAH8 entryIdentifier LogLine Mapping Undefined 2 LogLine2 1 LogLine Mapping Undefined 3 LogLine2 1 Mapping1 Undefined 2 1 Mapping Undefined 3 1 YnBsaXN0MDDUAQIDBAUGLC1YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkH CBMUGRoiJilVJG51bGzVCQoLDA0ODxAREllOU09wZXJhbmReTlNTZWxlY3Rvck5hbWVfEBBOU0V4cHJlc3Npb25UeXBlW05TQXJndW1lbnRzViRjbGFzc4ADgAIQBIAGgAhfEBRuZXdTZXNzaW9uSWRlbnRpZmllctMVCw0WFxhaTlNWYXJpYWJsZYAEEAKABVxlbnRpdHlQb2xpY3nSGxwdHlokY2xhc3NuYW1lWCRjbGFzc2VzXxAUTlNWYXJpYWJsZUV4cHJlc3Npb26jHyAhXxAUTlNWYXJpYWJsZUV4cHJlc3Npb25cTlNFeHByZXNzaW9uWE5TT2JqZWN00iMNJCVaTlMub2JqZWN0c6CAB9IbHCcoV05TQXJyYXmiJyHSGxwqK18QFE5TRnVuY3Rpb25FeHByZXNzaW9uoyogIV8QD05TS2V5ZWRBcmNoaXZlctEuL1Ryb290gAEACAARABoAIwAtADIANwBBAEcAUgBcAGsAfgCKAJEAkwCVAJcAmQCbALIAuQDEAMYAyADKANcA3ADnAPABBwELASIBLwE4AT0BSAFJAUsBUAFYAVsBYAF3AXsBjQGQAZUAAAAAAAACAQAAAAAAAAAwAAAAAAAAAAAAAAAAAAABlw== sessionIdentifier Mapping1 Undefined 2 1 ================================================ FILE: XPC Services/Historic Log File Manager/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleDisplayName Historic Log File Manager CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleVersion 1.0.0 NSHumanReadableCopyright Copyright © 2016 - 2018 Codeux Software, LLC. All rights reserved. XPCService ServiceType Application ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Headers/Private/IRCConnectionPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "RCMConnectionManagerProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface IRCConnection : NSObject - (instancetype)initWithConfig:(IRCConnectionConfig *)config onConnection:(NSXPCConnection *)connection NS_DESIGNATED_INITIALIZER; - (void)open; - (void)close; - (void)sendData:(NSData *)data bypassQueue:(BOOL)bypassQueue; - (void)enforceFloodControl; - (void)clearSendQueue; - (BOOL)exportSecureConnectionInformation:(RCMSecureConnectionInformationCompletionBlock)completionBlock error:(NSError * _Nullable * _Nullable)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Headers/Private/RCMConnectionManagerProtocol.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* *** XPC PROTOCOL HEADERS ARE PRIVATE *** */ NS_ASSUME_NONNULL_BEGIN @class IRCConnectionConfig; typedef void (^RCMSecureConnectionInformationCompletionBlock)( NSString * _Nullable policyName, tls_protocol_version_t ptocoolType, tls_ciphersuite_t cipherSuites, NSArray *certificateChain); #pragma mark - #pragma mark Server Protocol /* The server protocol is what the daemon responds to. */ @protocol RCMConnectionManagerServerProtocol @required - (void)openWithConfig:(IRCConnectionConfig *)config; - (void)close; /* -sendData: does not append \r\n. It is assumed client does that. */ - (void)sendData:(NSData *)data; - (void)sendData:(NSData *)data bypassQueue:(BOOL)bypassQueue; - (void)exportSecureConnectionInformation:(NS_NOESCAPE RCMSecureConnectionInformationCompletionBlock)completionBlock; - (void)enforceFloodControl; - (void)clearSendQueue; #pragma mark - #pragma mark Resource Usage - (void)enableAppNap; - (void)disableAppNap; - (void)enableSuddenTermination; - (void)disableSuddenTermination; @end #pragma mark - #pragma mark Client Protocol /* The client protocol is what Textual (the client) implements so that the daemon can communicate state with it. */ @protocol RCMConnectionManagerClientProtocol @required - (void)ircConnectionWillConnectToProxy:(NSString *)proxyHost port:(uint16_t)proxyPort; /* host is nil if we are connected to a proxy because we do not have enough context at the point this delegate method is called to know where the proxy itself connected. */ - (void)ircConnectionDidConnectToHost:(nullable NSString *)host; - (void)ircConnectionDidSecureConnectionWithProtocolType:(tls_protocol_version_t)protocolType cipherSuite:(tls_ciphersuite_t)cipherSuite; - (void)ircConnectionDidCloseReadStream; - (void)ircConnectionDidDisconnectWithError:(nullable NSError *)disconnectError; - (void)ircConnectionDidReceiveData:(NSData *)data; - (void)ircConnectionRequestInsecureCertificateTrust:(RCMTrustResponse)trustBlock; - (void)ircConnectionWillSendData:(NSData *)data; - (void)ircConnectionDidSendData; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Headers/Private/RCMProcessDelegatePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface RCMProcessDelegate : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Headers/Private/RCMProcessMainPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface RCMProcessMain : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithXPCConnection:(NSXPCConnection *)connection NS_DESIGNATED_INITIALIZER; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Headers/Private/RCMProcessPCHPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import #import #import "StaticDefinitions.h" #import "GCDAsyncSocket.h" #import "GCDAsyncSocketExtensions.h" #import "NSObjectHelperPrivate.h" #import "TLOLocalization.h" #import "TLOTimer.h" #import "IRCConnectionConfig.h" #import "IRCConnectionErrors.h" #import "IRCConnectionPrivate.h" #import "RCMConnectionManagerProtocol.h" #import "RCMProcessDelegatePrivate.h" #import "RCMProcessMainPrivate.h" ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Headers/Private/SwiftBridgingHeaderPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "RCMProcessPCHPrivate.h" #import ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/IRC/IRCConnection.swift ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 - 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ @objc(IRCConnection) final class Connection: NSObject, ConnectionSocketDelegate { fileprivate let config: IRCConnectionConfig fileprivate let socket: ConnectionSocket & ConnectionSocketProtocol fileprivate let serviceConnection: NSXPCConnection fileprivate var sendQueue: [Data] = [] fileprivate lazy var floodControlTimer: TLOTimer = { return TLOTimer(actionBlock: { [weak self] _ in self?.onFloodControlTimer() }, on: DispatchQueue.global(qos: .default)) }() fileprivate var floodControlCurrentMessageCount = 0 fileprivate var floodControlEnforced = false fileprivate var workerQueue: DispatchQueue? fileprivate var disconnectingManually = false enum ConnectionError : Error { /// socketError are errors returned by the connection library. /// For example: GCDAsyncSocket, Network.framework, etc. case socket(error: Error) // otherError are errors returned by ConnectionSocket instances. case other(message: String) /// invalidCertificate are errors returned when the connection /// cannot be secured because of problem with certificate. case badCertificate(failureReason: String) /// unableToSecure are errors returned when the connection /// cannot be secured for some reason. e.g. handshake failure case unableToSecure(failureReason: String) } // ConnectionError // MARK: - Initialization @objc(initWithConfig:onConnection:) init (with config: IRCConnectionConfig, on connection: NSXPCConnection) { self.config = config socket = ConnectionSocket.socket(with: config) serviceConnection = connection super.init() socket.delegate = self } // MARK: - Grand Central Dispatch fileprivate func destroyWorkerDispatchQueue() { workerQueue = nil } fileprivate func createWorkerDispatchQueue() { let workerQueueName = "Textual.IRCConnection.workerQueue.\(socket.uniqueIdentifier)" workerQueue = DispatchQueue(label: workerQueueName) } // MARK: - Open/Close @objc final func open() { Logging.defaultSubsystem?.debug("Opening connection \(self.socket.uniqueIdentifier, privacy: .public)...") if (socket.disconnected == false) { Logging.defaultSubsystem?.error("Already connected") return } createWorkerDispatchQueue() startFloodControlTimer() disconnectingManually = true socket.open() } @objc final func close() { Logging.defaultSubsystem?.debug("Closing connection \(self.socket.uniqueIdentifier, privacy: .public)...") if (socket.disconnected) { Logging.defaultSubsystem?.error("Not connected") return } floodControlEnforced = false clearSendQueue() stopFloodControlTimer() disconnectingManually = true socket.close() } final func resetState() { /* Method invoked when a disconnect occurs. */ /* disconnectingManually prevents us doing redundant work. */ if (disconnectingManually) { disconnectingManually = false } else { floodControlEnforced = false clearSendQueue() stopFloodControlTimer() } destroyWorkerDispatchQueue() } // MARK: - Send Queue fileprivate var sendQueueCount: Int { var sendQueueCount = 0 workerQueue?.sync { sendQueueCount = sendQueue.count } return sendQueueCount } fileprivate func nextEntryInSendQueue() -> Data? { var nextEntry: Data? workerQueue?.sync { nextEntry = sendQueue.first } return nextEntry } fileprivate func sendQueue(add data: Data) { workerQueue?.sync { sendQueue.append(data) } } fileprivate func sendQueue(remove data: Data) { workerQueue?.sync { if let index = sendQueue.firstIndex(of: data) { sendQueue.remove(at: index) } } } @objc final func clearSendQueue() { workerQueue?.sync { sendQueue.removeAll() } } @discardableResult fileprivate func tryToSend() -> Bool { if (socket.sending) { return false } if (sendQueueCount == 0) { return false } if (floodControlEnforced) { if (floodControlCurrentMessageCount >= config.floodControlMaximumMessages) { return false } } floodControlCurrentMessageCount += 1 sendNextLine() return true } fileprivate func sendNextLine() { guard let line = nextEntryInSendQueue() else { return } send(line, removeFromQueue: true) } @objc(sendData:bypassQueue:) final func send(_ data: Data, bypassQueue: Bool = false) { if (socket.disconnected) { Logging.defaultSubsystem?.error("Cannot send data while disconnected") return } if (bypassQueue) { send(data, removeFromQueue: false) return } sendQueue(add: data) tryToSend() } fileprivate func send(_ data: Data, removeFromQueue: Bool = false) { if (removeFromQueue) { sendQueue(remove: data) } socket.write(data) } // MARK: - Flood Control @objc final func enforceFloodControl() { floodControlEnforced = true } fileprivate func startFloodControlTimer() { if (floodControlTimer.timerIsActive) { return } let timerInterval = Double(config.floodControlDelayInterval) floodControlTimer.start(timerInterval, onRepeat: true) } fileprivate func stopFloodControlTimer() { if (floodControlTimer.timerIsActive == false) { return } floodControlTimer.stop() } fileprivate func onFloodControlTimer() { floodControlCurrentMessageCount = 0 while (tryToSend()) { } } // MARK: - Socket Proxy @objc(exportSecureConnectionInformation:error:) final func exportSecureConnectionInformation(to receiver: RCMSecureConnectionInformationCompletionBlock) throws { try socket.exportSecureConnectionInformation(to: receiver) } // MARK: - Socket Delegate final var remoteObjectProxy: RCMConnectionManagerClientProtocol { return serviceConnection.remoteObjectProxy as! RCMConnectionManagerClientProtocol } final func connection(_ connection: ConnectionSocket, willConnectToProxy address: String, on port: UInt16) { remoteObjectProxy.ircConnectionWillConnect(toProxy: address, port: port) } final func connection(_ connection: ConnectionSocket, willConnectTo address: String, on port: UInt16) { } final func connection(_ connection: ConnectionSocket, didConnectTo address: String?) { remoteObjectProxy.ircConnectionDidConnect(toHost: address) } final func connection(_ connection: ConnectionSocket, securedWith protocol: tls_protocol_version_t, cipherSuite: tls_ciphersuite_t) { remoteObjectProxy.ircConnectionDidSecureConnection(withProtocolType: `protocol`, cipherSuite: cipherSuite) } final func connection(_ connection: ConnectionSocket, requiresTrust response: @escaping (Bool) -> Void) { remoteObjectProxy.ircConnectionRequestInsecureCertificateTrust(response) } final func connectionClosedReadStream(_ connection: ConnectionSocket) { remoteObjectProxy.ircConnectionDidCloseReadStream() } final func connectionDisconnected(_ connection: ConnectionSocket) { resetState() remoteObjectProxy.ircConnectionDidDisconnectWithError(nil) } final func connection(_ connection: ConnectionSocket, disconnectedWith error: ConnectionError) { resetState() remoteObjectProxy.ircConnectionDidDisconnectWithError(error as NSError) } final func connection(_ connection: ConnectionSocket, received data: Data) { remoteObjectProxy.ircConnectionDidReceive(data) } final func connection(_ connection: ConnectionSocket, willSend data: Data) { remoteObjectProxy.ircConnectionWillSend(data) } final func connectionDidSend(_ connection: ConnectionSocket) { remoteObjectProxy.ircConnectionDidSendData() tryToSend() } } // MARK: - Extensions typealias ConnectionError = Connection.ConnectionError extension ConnectionError: CustomNSError { /* Error domain and codes are defined in IRCConnectionErrors.h/m */ static let errorDomain = ConnectionErrorDomain var errorCode: Int { let errorCode: ConnectionErrorCode switch self { case .socket(_): errorCode = .socket case .other(_): errorCode = .other case .badCertificate(_): errorCode = .badCertificate case .unableToSecure(_): errorCode = .unableToSecure } return Int(errorCode.rawValue) } var errorUserInfo: [String : Any] { var userInfo: [String : Any] = [:] if let errorDescription = errorDescription { userInfo[NSLocalizedDescriptionKey] = errorDescription } // While we don't make use of it right now, pass the original // error object inside the user info dictionary because at // a later time, we may be interested in its contents. if case let .socket(error) = self { userInfo["UnderlyingSocketError"] = error } return userInfo } } extension ConnectionError: LocalizedError { var errorDescription: String? { switch self { case .socket(let error): /* The underlying socket error is almost always an NSError which means we can just ask for its localized description. */ return error.localizedDescription case .other(let message), .badCertificate(let message), .unableToSecure(let message): return message } } } fileprivate extension ConnectionSocket { static func socket(with config: IRCConnectionConfig) -> ConnectionSocket & ConnectionSocketProtocol { #if canImport(Network) if #available(macOS 10.14, *) { if (config.connectionPrefersModernSockets) { return ConnectionSocketNWF(with: config) } } #endif return ConnectionSocketClassic(with: config) } } ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/IRC/IRCConnectionSocket.swift ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* ConnectionSocket is subclassed to implement the connection logic. One subclass uses GCDAsyncSocket which isn't designed for Swift. To accommodate some of its features, we must have our base class inherit from NSObject or all hell will break loose. */ class ConnectionSocket: NSObject { weak var delegate: ConnectionSocketDelegate? final private(set) var config: IRCConnectionConfig final let uniqueIdentifier: String var connecting = false var connected = false var connectedWithClientSideCertificate = false var disconnecting = false var disconnected: Bool { return (connecting == false && connected == false) } var secured = false var sending = false var EOFReceived = false var alternateDisconnectError: ConnectionError? final let torProxyTypeAddress = "127.0.0.1" final let torProxyTypePort: UInt16 = 9150 final let maximumDataLength = (1000 * 1000 * 100) // 100 megabytes init (with config: IRCConnectionConfig) { self.config = config uniqueIdentifier = UUID().uuidString super.init() } func resetState() { connecting = false connected = false connectedWithClientSideCertificate = false disconnecting = false secured = false sending = false EOFReceived = false alternateDisconnectError = nil } func tlsVerify(_ trust: SecTrust, response: @escaping RCMTrustResponse) { if (config.connectionShouldValidateCertificateChain == false) { response(true) return } var evaluationResult: SecTrustResultType = .invalid let evaluationStatus = SecTrustEvaluate(trust, &evaluationResult) if (evaluationStatus == errSecSuccess) { if (evaluationResult == .unspecified || evaluationResult == .proceed) { response(true) return } else if (evaluationResult == .recoverableTrustFailure) { delegate?.connection(self, requiresTrust: response) return } } response(false) } final var clientSideCertificate: (identity: SecIdentity, certificate: SecCertificate)? { guard let certificateDataIn = config.identityClientSideCertificate else { return nil } /* ====================================== */ var keychainRef: SecKeychainItem? let certificateDataInRef = certificateDataIn as CFData var status = SecKeychainItemCopyFromPersistentReference(certificateDataInRef, &keychainRef) if (status != noErr) { Logging.defaultSubsystem?.error("Operation Failed (1): \(status, privacy: .public)") return nil } /* "A SecKeychainItem object for a certificate that is stored in a keychain can be safely cast to a SecCertificate for use with Certificate, Key, and Trust Services." */ /* Contrary to the statement above, as stated in documentation, casting was crashing. This is a workaround until that's fixed. */ let certificateRef = unsafeBitCast(keychainRef, to: SecCertificate.self) /* ====================================== */ var identityRef: SecIdentity? status = SecIdentityCreateWithCertificate(nil, certificateRef, &identityRef) if (status != noErr) { Logging.defaultSubsystem?.error("Operation Failed (2): \(status, privacy: .public)") return nil } /* ====================================== */ return (identity: identityRef!, certificate: certificateRef) } final func changeProxy(to type: IRCConnectionProxyType = .none, at host: String? = nil, on port: UInt16 = 0, username: String? = nil, password: String? = nil) { let mutableConfig: IRCConnectionConfigMutable = config.mutableCopy() as! IRCConnectionConfigMutable mutableConfig.proxyAddress = host mutableConfig.proxyPort = port mutableConfig.proxyType = type mutableConfig.proxyUsername = username mutableConfig.proxyPassword = password config = mutableConfig } final func changeProxyToTor() { changeProxy(to: .socks5, at: torProxyTypeAddress, on: torProxyTypePort) } final func changeProxyToNone() { changeProxy() } } extension ConnectionError { init (socketError: Error) { self = .socket(error: socketError) } init (otherError message: String) { self = .other(message: message) } init? (tlsError error: Error) { if (RCMSecureTransport.isTLSError(error) == false) { return nil } self.init(tlsError: error.code) } /// init(tlsError:) returns .unableToSecure("Unknown") for out of range error codes init (tlsError errorCode: Int) { if let certError = RCMSecureTransport.description(forBadCertificateErrorCode: errorCode) { self = .badCertificate(failureReason: certError) return } let tlsError = RCMSecureTransport.description(forErrorCode: errorCode) self = .unableToSecure(failureReason: tlsError) } } protocol ConnectionSocketDelegate: AnyObject { func connection(_ connection: ConnectionSocket, willConnectToProxy address: String, on port: UInt16) func connection(_ connection: ConnectionSocket, willConnectTo address: String, on port: UInt16) func connection(_ connection: ConnectionSocket, didConnectTo address: String?) // address is nil when connecting to proxy func connection(_ connection: ConnectionSocket, securedWith protocol: tls_protocol_version_t, cipherSuite: tls_ciphersuite_t) func connection(_ connection: ConnectionSocket, requiresTrust response: @escaping (Bool) -> Void) func connectionClosedReadStream(_ connection: ConnectionSocket) func connectionDisconnected(_ connection: ConnectionSocket) func connection(_ connection: ConnectionSocket, disconnectedWith error: ConnectionError) func connection(_ connection: ConnectionSocket, received data: Data) func connection(_ connection: ConnectionSocket, willSend data: Data) func connectionDidSend(_ connection: ConnectionSocket) } protocol ConnectionSocketProtocol { /// Logic for opening socket func open() /// Logic for closing socket func close() func close(with error: String) func close(with error: ConnectionError) /// Logic for writing data (sending) func write(_ data: Data) /// Logic for waiting for data (receiving) func read() /// Logic for reading data from socket (receiving) func readIn(_ data: Data) /// Logic for providing upstream with information /// about the secured connection including policy name, /// protocol version, cipher suite, and certificates. func exportSecureConnectionInformation(to receiver: RCMSecureConnectionInformationCompletionBlock) throws /// TLS Information var tlsNegotiatedProtocol: tls_protocol_version_t? { get } var tlsNegotiatedCipherSuite: tls_ciphersuite_t? { get } var tlsCertificateChainData: [Data]? { get } var tlsPolicyName: String? { get } } extension ConnectionSocketProtocol where Self: ConnectionSocket { func close(with error: String) { let errorEnum = ConnectionError.other(message: error) close(with: errorEnum) } func close(with error: ConnectionError) { if (disconnected || disconnecting) { return } alternateDisconnectError = error close() } func exportSecureConnectionInformation(to receiver: RCMSecureConnectionInformationCompletionBlock) throws { let policyName = tlsPolicyName let protocolType = tlsNegotiatedProtocol ?? tls_protocol_version_unknown let cipherSuite = tlsNegotiatedCipherSuite ?? tls_ciphersuite_unknown let certificateChain = tlsCertificateChainData ?? [] receiver(policyName, protocolType, cipherSuite, certificateChain) } } ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/IRC/IRCConnectionSocketClassic.swift ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018, 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ final class ConnectionSocketClassic: ConnectionSocket, ConnectionSocketProtocol, GCDAsyncSocketDelegate { fileprivate enum Tag : Int { case none = 0 case socksProxyOpen = 10100 case socksProxyConnect = 10200 case socksProxyConnectReplyOne = 10300 case socksProxyAuthenticateUser = 10500 } fileprivate enum Timeout : Double { case normal = 30.0 case none = -1.0 } fileprivate let httpHeaderResponseStatusRegularExpression = "^HTTP\\/([1-2]{1})(\\.([0-2]{1}))?\\s([0-9]{3,4})\\s(.*)$" fileprivate var socketDelegateQueue: DispatchQueue? fileprivate var socketReadWriteQueue: DispatchQueue? fileprivate var workerQueue: DispatchQueue? fileprivate var connection: GCDAsyncSocket? fileprivate let readDelimiter = Data([0x0a]) // \n // MARK: - Grand Central Dispatch fileprivate func destroyDispatchQueues() { socketDelegateQueue = nil socketReadWriteQueue = nil } fileprivate func createDispatchQueues() { let socketDelegateQueueName = "Textual.ConnectionSocket.socketDelegateQueue.\(uniqueIdentifier)" socketDelegateQueue = DispatchQueue(label: socketDelegateQueueName) let socketReadWriteQueueName = "Textual.ConnectionSocket.socketReadWriteQueue.\(uniqueIdentifier)" socketReadWriteQueue = DispatchQueue(label: socketReadWriteQueueName) } // MARK: - Open/Close Socket func open() { if (disconnected == false || disconnecting) { return } createDispatchQueues() let connection = GCDAsyncSocket(delegate: self, delegateQueue: socketDelegateQueue, socketQueue: socketReadWriteQueue) connection.useStrictTimers = true switch config.addressType { case .v4: connection.isIPv6Enabled = false case .v6: connection.isIPv4Enabled = false default: connection.isIPv4PreferredOverIPv6 = false } self.connection = connection if (proxyConfigured) { /* populateSystemSocksProxy() does not assign an error for non-fatal failures which means this value should be treated as optional. */ var proxyPopulateError: String? if (populateSystemSocksProxy(failureReason: &proxyPopulateError) == false) { if let error = proxyPopulateError { Logging.defaultSubsystem?.error("\(error, privacy: .public)") } } else { let proxyAddress = config.proxyAddress! let proxyPort = config.proxyPort delegate?.connection(self, willConnectToProxy: proxyAddress, on: proxyPort) connect(to: proxyAddress, on: proxyPort) return } } let serverAddress = config.serverAddress let serverPort = config.serverPort delegate?.connection(self, willConnectTo: serverAddress, on: serverPort) connect(to: serverAddress, on: serverPort) } fileprivate func connect(to host: String, on port: UInt16) { connecting = true do { try connection?.connect(toHost: host, onPort: port, withTimeout: Timeout.normal.rawValue) } catch { let socketError = ConnectionError(socketError: error) close(with: socketError) } } func close() { if (disconnected || disconnecting) { return } disconnecting = true connection?.disconnect() } override func resetState() { super.resetState() connection = nil destroyDispatchQueues() } // MARK: - Socket Read & Write func write(_ data: Data) { if (connected == false || disconnecting) { return } /* We only allow one write a time */ if (sending) { return } sending = true delegate?.connection(self, willSend: data) connection?.write(data, withTimeout: Timeout.none.rawValue, tag: Tag.none.rawValue) } func read() { if (connected == false || disconnecting) { return } connection?.readData(to: readDelimiter, withTimeout: Timeout.none.rawValue, maxLength: UInt(maximumDataLength), tag: Tag.none.rawValue) } func readIn(_ data: Data) { if (disconnected || disconnecting) { return } /* We read until \n appears. Data returned by socket will include the \n and \r if it's present. We therefore trim the data of \r and \n when we read it in. */ let trimmedData = data.withoutNewlinesAtEnd delegate?.connection(self, received: trimmedData) } // MARK: - Properties final var connectedHost: String? { if (proxyInUse) { return nil } return connection?.connectedHost } fileprivate func beginTLSNegotiation() { if (config.connectionPrefersSecuredConnection == false) { return } /* This makes me cry */ var settings:[String : NSObject] = [ GCDAsyncSocketManuallyEvaluateTrust : NSNumber(value: true), GCDAsyncSocketSSLProtocolVersionMin : NSNumber(value: RCMSecureTransport.minimumDeprecatedProtocol.rawValue), kCFStreamSSLIsServer as String : NSNumber(value: false), kCFStreamSSLPeerName as String : config.serverAddress as NSString ] if (config.cipherSuites != .none) { settings[GCDAsyncSocketSSLCipherSuites] = RCMSecureTransport.cipherSuites(in: config.cipherSuites, includeDeprecated: (config.connectionPrefersModernCiphersOnly == false)) as NSArray } if let certificate = clientSideCertificate { settings[kCFStreamSSLCertificates as String] = NSArray(objects: certificate.identity, certificate.certificate) connectedWithClientSideCertificate = true } connection?.startTLS(settings) } fileprivate func onConnect() { beginTLSNegotiation() connecting = false connected = true read() delegate?.connection(self, didConnectTo: connectedHost) } fileprivate func onSecured() { secured = true let protocolType = tlsNegotiatedProtocol ?? tls_protocol_version_unknown let cipherSuite = tlsNegotiatedCipherSuite ?? tls_ciphersuite_unknown delegate?.connection(self, securedWith: protocolType, cipherSuite: cipherSuite) } fileprivate func onDisconnect(with error: Error?) { defer { resetState() } var errorPayload: ConnectionError? if let alternateError = alternateDisconnectError { errorPayload = alternateError } else if let err = error { if let tlsError = ConnectionError(tlsError: err) { errorPayload = tlsError } else if (err.code != errSSLClosedGraceful) { errorPayload = ConnectionError(socketError: err) } } if (errorPayload == nil) { delegate?.connectionDisconnected(self) } else { delegate?.connection(self, disconnectedWith: errorPayload!) } } // MARK: - GCDAsyncSocketDelegate final func socket(_ sock: GCDAsyncSocket, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) { tlsVerify(trust) { (underlyingResponse) in completionHandler(underlyingResponse) } } final func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) { if (proxyInUse) { do { try openProxy() } catch let error as ConnectionError { close(with: error) } catch { fatalError("Unexpected error: \(error)") } return } onConnect() } final func socketDidCloseReadStream(_ sock: GCDAsyncSocket) { EOFReceived = true delegate?.connectionClosedReadStream(self) } final func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) { onDisconnect(with: err) } final func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) { if (proxyInUse) { do { /* proxyRead() returns true if it swallows the data such as when it is talking directly to the proxy during negotiations. */ if (try proxyRead(data, with: Tag(rawValue: tag) ?? .none)) { return } } catch let error as ConnectionError { close(with: error) return } catch { fatalError("Unexpected error: \(error)") } } readIn(data) read() } final func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) { sending = false delegate?.connectionDidSend(self) } final func socketDidSecure(_ sock: GCDAsyncSocket) { onSecured() } // MARK: - Security var tlsNegotiatedProtocol: tls_protocol_version_t? { return connection?.tlsNegotiatedProtocol } var tlsNegotiatedCipherSuite: tls_ciphersuite_t? { return connection?.tlsNegotiatedCipherSuite } var tlsCertificateChainData: [Data]? { return connection?.tlsCertificateChainData } var tlsPolicyName: String? { return connection?.tlsPolicyName } // MARK: - SOCKS Proxy Support fileprivate var proxyConfigured: Bool { let proxyType = config.proxyType return (proxyType == .automatic || proxyType == .socks4 || proxyType == .socks5 || proxyType == .tor || proxyType == .HTTP) } fileprivate var proxyInUse: Bool { let proxyType = config.proxyType return (proxyType == .socks4 || proxyType == .socks5 || proxyType == .HTTP) } fileprivate var proxyCanAuthenticate: Bool { return (config.proxyUsername?.isEmpty == false && config.proxyPassword?.isEmpty == false) } fileprivate func populateSystemSocksProxy(failureReason: inout String?) -> Bool { let proxyType = config.proxyType if (proxyType == .automatic) { /* Being unable to read proxy values is considered non-fatal error which why an failure reason is never assigned. */ guard let proxySettings = SCDynamicStoreCopyProxies(nil) as? [String : AnyObject] else { return false } if (proxySettings.bool(for: "SOCKSEnable") == false) { return false } guard let proxyHost = proxySettings.string(for: "SOCKSProxy") else { return false } if (proxyHost.isEmpty) { return false } let proxyPort = proxySettings.integer(for: "SOCKSPort") if (proxyPort.isValidInternetPort == false) { return false } var proxyPassword: String? let proxyUsername = proxySettings.string(for: "SOCKSUser") if proxyUsername?.isEmpty == false { let queryParameters:[CFString : CFTypeRef] = [ kSecClass : kSecClassInternetPassword, kSecAttrServer : proxyHost as CFString, kSecAttrProtocol : kSecAttrProtocolSOCKS, kSecReturnData : kCFBooleanTrue, kSecMatchLimit : kSecMatchLimitOne ] var queryResultRef: CFTypeRef? let queryStatus = SecItemCopyMatching(queryParameters as CFDictionary, &queryResultRef) if (queryStatus != noErr) { failureReason = "SOCKS Error: Textual encountered a problem trying to retrieve the SOCKS proxy password from System Settings" return false } let proxyPasswordData = queryResultRef as! Data proxyPassword = String(data: proxyPasswordData, encoding: .utf8) } // proxyUsername changeProxy(to: .socks5, at: proxyHost, on: UInt16(proxyPort), username: proxyUsername, password: proxyPassword) } else if (proxyType == .tor) { changeProxyToTor() } return true } fileprivate func openProxy() throws { let proxyType = config.proxyType switch proxyType { case .socks4: try socks4ProxyOpen() case .socks5: socks5ProxyOpen() case .HTTP: httpProxyOpen() default: return } // switch() } /* Boolean return value indicates whether the data was successfully read as data related to the proxy. When false is returned, the data is passed upstream as normal data to read. */ fileprivate func proxyRead(_ data: Data, with tag: Tag = .none) throws -> Bool { let proxyType = config.proxyType switch proxyType { case .socks4: return try socks4ProxyRead(data, with: tag) case .socks5: return try socks5ProxyRead(data, with: tag) case .HTTP: return try httpProxyRead(data, with: tag) default: return false } // switch() } // MARK: - SOCKS4 and SOCKS5 fileprivate func socksProxyConnect() throws { // // Packet layout for SOCKS4 connect: // // +----+----+---------+-------------------+---------+....+----+ // NAME | VN | CD | DSTPORT | DSTIP | USERID |NULL| // +----+----+---------+-------------------+---------+....+----+ // SIZE 1 1 2 4 variable 1 // // --------------------------------------------------------------------------- // // Packet layout for SOCKS5 connect: // // +-----+-----+-----+------+------+------+ // NAME | VER | CMD | RSV | ATYP | ADDR | PORT | // +-----+-----+-----+------+------+------+ // SIZE | 1 | 1 | 1 | 1 | var | 2 | // +-----+-----+-----+------+------+------+ // var destination = "" let socksVersion = config.proxyType if (socksVersion == .socks4) { guard let resolvedAddress = socks4ConnectAddress else { throw ConnectionError(otherError: "SOCKS4 Error: Unable to resolve an IPv4 address to connect to") } destination = resolvedAddress } else { destination = config.serverAddress } /* "...big-endian byte order is also referred to as network byte order... */ let destinationPortBytes = config.serverPort.bigEndian.data /* Assemble the packet of data that will be sent */ var packetData = Data() /* SOCKS version to use */ if (socksVersion == .socks5) { packetData.append([0x05], count: 1) } else { packetData.append([0x04], count: 1) } /* Type of connection (the command) */ packetData.append([0x01], count: 1) if (socksVersion == .socks5) { /* Reserved value that must be 0 for SOCKS5 */ packetData.append([0x00], count: 1) /* The address */ if let IPv4Bytes = destination.IPv6AddressBytes { packetData.append([0x04], count: 1) packetData.append(IPv4Bytes) } else if let IPv6Bytes = destination.IPv4AddressBytes { packetData.append([0x01], count: 1) packetData.append(IPv6Bytes) } else { packetData.append([0x03], count: 1) guard let addressBytes = destination.data(using: .ascii) else { throw ConnectionError(otherError: "SOCKS5 Error: Unable to convert address into a ASCII fragment") } let addressBytesLength = addressBytes.count if (addressBytesLength > UINT8_MAX) { throw ConnectionError(otherError: "SOCKS5 Error: Connection address length cannot exceed \(UINT8_MAX) characters") } packetData.append([UInt8(addressBytesLength)], count: 1) packetData.append(addressBytes) } // Address packetData.append(destinationPortBytes) } else // .socks5 { packetData.append(destinationPortBytes) guard let addressBytes = destination.IPv4AddressBytes else { throw ConnectionError(otherError: "SOCKS4 Error: Unable to convert address into network bytes") } packetData.append(addressBytes) packetData.append([0x00], count: 1) } /* Write the packet to the socket */ connection?.write(packetData, withTimeout: Timeout.none.rawValue, tag: Tag.socksProxyConnect.rawValue) // // Packet layout for SOCKS4 connect response: // // +----+----+----+----+----+----+----+----+ // NAME | VN | CD | DSTPORT | DSTIP | // +----+----+----+----+----+----+----+----+ // SIZE 1 1 2 4 // // Packet layout for SOCKS5 connect response: // // +-----+-----+-----+------+------+------+ // NAME | VER | REP | RSV | ATYP | ADDR | PORT | // +-----+-----+-----+------+------+------+ // SIZE | 1 | 1 | 1 | 1 | var | 2 | // +-----+-----+-----+------+------+------+ // /* Wait for a response from the SOCKS server */ connection?.readData(withTimeout: Timeout.normal.rawValue, tag: Tag.socksProxyConnectReplyOne.rawValue) } // MARK: - SOCKS5 fileprivate func socks5ProxyOpen() { socks5ProxySendGreeting() } fileprivate func socks5ProxyRead(_ data: Data, with tag: Tag = .none) throws -> Bool { if (tag == .socksProxyOpen) { if (data.count != 2) { throw ConnectionError(otherError: "SOCKS5 Error: Server responded with a malformed packet") } let version = data[0] let method = data[1] if (version != 5) { throw ConnectionError(otherError: "SOCKS5 Error: Server greeting reply contained incorrect version number") } switch method { case 0: try socksProxyConnect() case 2: if (proxyCanAuthenticate) { try socks5ProxyUserAuthentication() } else { throw ConnectionError(otherError: "SOCKS5 Error: Server requested that we authenticate but a username and/or password is not configured") } default: throw ConnectionError(otherError: "SOCKS5 Error: Server requested authentication method that is not supported") } return true } else if (tag == .socksProxyConnectReplyOne) { if (data.count <= 8) { // first 4 bytes + 2 for port throw ConnectionError(otherError: "SOCKS5 Error: Server responded with a malformed packet") } let version = data[0] let reply = data[1] if (version == 5 && reply == 0) { onConnect() } else { switch reply { case 1: throw ConnectionError(otherError: "SOCKS5 Error: General SOCKS server failure") case 2: throw ConnectionError(otherError: "SOCKS5 Error: Connection not allowed by ruleset") case 3: throw ConnectionError(otherError: "SOCKS5 Error: Network unreachable") case 4: throw ConnectionError(otherError: "SOCKS5 Error: Host unreachable") case 5: throw ConnectionError(otherError: "SOCKS5 Error: Connection refused") case 6: throw ConnectionError(otherError: "SOCKS5 Error: Time to live (TTL) expired") case 7: throw ConnectionError(otherError: "SOCKS5 Error: Command not supported") case 8: throw ConnectionError(otherError: "SOCKS5 Error: Address type not supported") default: throw ConnectionError(otherError: "SOCKS5 Error: Unknown SOCKS error") } } return true } else if (tag == .socksProxyAuthenticateUser) { // // Server response for username/password authentication: // // field 1: version, 1 byte // field 2: status code, 1 byte. // 0x00 = success // any other value = failure, connection must be closed // if (data.count != 2) { throw ConnectionError(otherError: "SOCKS5 Error: Server responded with a malformed packet") } let status = data[1] if (status == 0x00) { try socksProxyConnect() } else { throw ConnectionError(otherError: "SOCKS5 Error: Authentication failed for unknown reason") } return true } return false /* Read not handled here */ } fileprivate func socks5ProxySendGreeting() { // // Packet layout for SOCKS5 greeting: // // +-----+-----------+---------+ // NAME | VER | NMETHODS | METHODS | // +-----+-----------+---------+ // SIZE | 1 | 1 | 1 - 255 | // +-----+-----------+---------+ // /* Assemble the packet of data that will be sent */ var packetData = Data() if (proxyCanAuthenticate == false) { /* Send instructions that we are asking for version 5 of the SOCKS protocol with one authentication method: anonymous access */ packetData.append([0x05, 0x01, 0x00], count: 3) } else { /* Send instructions that we are asking for version 5 of the SOCKS protocol with two authentication methods: anonymous access and password based. */ packetData.append([0x05, 0x02, 0x00, 0x02], count: 4) } /* Write the packet to the socket */ connection?.write(packetData, withTimeout: Timeout.none.rawValue, tag: Tag.socksProxyOpen.rawValue) // // Packet layout for SOCKS5 greeting response: // // +-----+--------+ // NAME | VER | METHOD | // +-----+--------+ // SIZE | 1 | 1 | // +-----+--------+ // /* Wait for a response from the SOCKS server */ connection?.readData(withTimeout: Timeout.normal.rawValue, tag: Tag.socksProxyOpen.rawValue) } // // For username/password authentication the client's authentication request is // // field 1: version number, 1 byte (must be 0x01) // field 2: username length, 1 byte // field 3: username // field 4: password length, 1 byte // field 5: password // fileprivate func socks5ProxyUserAuthentication() throws { /* Assemble the packet of data that will be sent */ guard let usernameData = config.proxyUsername!.data(using: .utf8) else { throw ConnectionError(otherError: "SOCKS5 Error: Unable to convert username into a UTF-8 fragment") } let usernameLength = usernameData.count if (usernameLength > UINT8_MAX) { throw ConnectionError(otherError: "SOCKS5 Error: Username length cannot exceed \(UINT8_MAX) characters") } guard let passwordData = config.proxyPassword!.data(using: .utf8) else { throw ConnectionError(otherError: "SOCKS5 Error: Unable to convert password into a UTF-8 fragment") } let passwordLength = passwordData.count if (passwordLength > UINT8_MAX) { throw ConnectionError(otherError: "SOCKS5 Error: Password length cannot exceed \(UINT8_MAX) characters") } var authData = Data(capacity: 1 + 1 + usernameLength + 1 + passwordLength) authData.append([0x01], count: 1) authData.append([UInt8(usernameLength)], count: 1) authData.append(usernameData) authData.append([UInt8(passwordLength)], count: 1) authData.append(passwordData) /* Write the packet to the socket */ connection?.write(authData, withTimeout: Timeout.none.rawValue, tag: Tag.socksProxyAuthenticateUser.rawValue) /* Wait for a response from the SOCKS server */ connection?.readData(withTimeout: Timeout.none.rawValue, tag: Tag.socksProxyAuthenticateUser.rawValue) } // MARK: - SOCKS4 fileprivate func socks4ProxyOpen() throws { try socksProxyConnect() } fileprivate func socks4ProxyRead(_ data: Data, with tag: Tag = .none) throws -> Bool { if (tag != .socksProxyConnectReplyOne) { return false /* Read not handled here */ } if (data.count != 8) { throw ConnectionError(otherError: "SOCKS4 Error: Server responded with a malformed packet") } let reply = data[1] switch reply { case 0x5a: onConnect() case 0x5b: throw ConnectionError(otherError: "SOCKS4 Error: Request rejected or failed") case 0x5c: throw ConnectionError(otherError: "SOCKS4 Error: Request failed because client is not running an identd (or not reachable from server)") case 0x5d: throw ConnectionError(otherError: "SOCKS4 Error: Request failed because client's identd could not confirm the user ID string in the request") default: throw ConnectionError(otherError: "SOCKS4 Error: Server replied with unknown status code") } return true } fileprivate var socks4ConnectAddress: String? { /* SOCKS4 proxies do not support anything other than IPv4 addresses (unless you support SOCKS4a, which Textual does not) which means we perform manual DNS lookup for SOCKS4 and rely on end-point proxy to perform lookup when using other proxy types. */ let serverAddress = config.serverAddress if (serverAddress.isIPv4Address) { return serverAddress } else if (serverAddress.isIPv6Address) { return nil } guard let resolvedAddresses = try? GCDAsyncSocket.lookupHost(serverAddress, port: config.serverPort) as? [Data] else { return nil } let resolvedAddress4 = resolvedAddresses.first { GCDAsyncSocket.isIPv4Address($0) } if (resolvedAddress4 == nil) { return nil } return GCDAsyncSocket.host(fromAddress: resolvedAddress4!) } // MARK: - HTTP Proxy fileprivate func httpProxyOpen() { let connectionAddress = config.serverAddress let connectionPort = config.serverPort var connectionAddressCombined = "" if (connectionAddress.isIPv6Address) { connectionAddressCombined = "[\(connectionAddress)]:\(connectionPort)" // IPv6 requires brackets } else { connectionAddressCombined = "\(connectionAddress):\(connectionPort)" } let connectCommand = "CONNECT \(connectionAddressCombined) HTTP/1.1\r\n\r\n" /* Pass the data along to the HTTP server */ let connectCommandData = connectCommand.data(using: .ascii) connection?.write(connectCommandData!, withTimeout: Timeout.none.rawValue, tag: Tag.socksProxyOpen.rawValue) /* Read until the end of the HTTP header response */ let responseTerminatorData = "\r\n\r\n".data(using: .ascii) connection?.readData(to: responseTerminatorData!, withTimeout: Timeout.normal.rawValue, tag: Tag.socksProxyOpen.rawValue) } fileprivate func httpProxyRead(_ data: Data, with tag: Tag = .none) throws -> Bool { if (tag != .socksProxyOpen) { return false /* Read not handled here */ } /* Given data, turn it into string and perform basic validation */ guard let dataAsString = String(data: data, encoding: .utf8) else { throw ConnectionError(otherError: "HTTP Error: Unable to read data") } let headerComponents = dataAsString.components(separatedBy: "\r\n") if (headerComponents.count <= 2) { throw ConnectionError(otherError: "HTTP Error: Server responded with a malformed packet") } /* Try our best to extract the status code from the response */ let statusResponse = headerComponents[0] // It is possible to split the response into its components using // the space character but by using regular expression we are not // only getting the components, we are also validating its format. let statusResponseRegexRange = NSMakeRange(0, statusResponse.count) let statusResponseRegex = try! NSRegularExpression(pattern: httpHeaderResponseStatusRegularExpression, options: []) let statusResponseRegexResult = statusResponseRegex.firstMatch(in: statusResponse, options: [], range: statusResponseRegexRange) if (statusResponseRegexResult?.numberOfRanges != 6) { throw ConnectionError(otherError: "HTTP Error: Server responded with a malformed packet") } // // Index values: // // Complete Line (0): HTTP/1.1 200 Connection established // Major Version (1): 1 // Minor Version (2): .1 // Minor Version (3): 1 // Status Code (4): 200 // Status Message (5): Connection established // let statusCodeRange = statusResponseRegexResult?.range(at: 4) let statusCode = statusResponse.substring(with: statusCodeRange!)! if (Int(statusCode) == 200) { onConnect() } else { let statusMessageRange = statusResponseRegexResult?.range(at: 5) let statusMessage = statusResponse.substring(with: statusMessageRange!)! throw ConnectionError(otherError: "HTTP Error: HTTP proxy server returned status code \(statusCode) with the message “\(statusMessage)”") } return true } } ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/IRC/IRCConnectionSocketNWF.swift ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018, 2019 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #if canImport(Network) import Network @available(macOS 10.14, *) final class ConnectionSocketNWF: ConnectionSocket, ConnectionSocketProtocol { fileprivate var readInBuffer: Data? fileprivate var connection: NWConnection? fileprivate var socketDelegateQueue: DispatchQueue? fileprivate var trustRef: SecTrust? // MARK: - Grand Central Dispatch fileprivate func destroyDispatchQueues() { socketDelegateQueue = nil } fileprivate func createDispatchQueues() { let socketDelegateQueueName = "Textual.ConnectionSocket.socketDelegateQueue.\(uniqueIdentifier)" socketDelegateQueue = DispatchQueue(label: socketDelegateQueueName) } // MARK: - Open/Close Socket fileprivate var constructedParameters: NWParameters { var parameters: NWParameters if (config.connectionPrefersSecuredConnection) { parameters = NWParameters(tls: constructedTLSOptions) } else { parameters = .tcp } parameters.preferNoProxies = (config.proxyType == .none) if let internetProtocol = parameters.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options { switch config.addressType { case .v4: internetProtocol.version = .v4 case .v6: internetProtocol.version = .v6 default: break } } return parameters } fileprivate var constructedTLSOptions: NWProtocolTLS.Options { let tlsOptions = NWProtocolTLS.Options() let secOptions = tlsOptions.securityProtocolOptions if let localIdentity = tlsLocalIdentity { sec_protocol_options_set_local_identity(secOptions, localIdentity) } if (config.cipherSuites == .none) { sec_protocol_options_append_tls_ciphersuite_group(secOptions, .default) } else { RCMSecureTransport.appendCipherSuites(in: config.cipherSuites, includeDeprecated: (config.connectionPrefersModernCiphersOnly == false), to: secOptions) } sec_protocol_options_set_min_tls_protocol_version(secOptions, RCMSecureTransport.minimumProtocolType) sec_protocol_options_set_verify_block(secOptions, { [weak self] (_, trust, completionBlock) in self?.tlsVerifySecProtocol(trust, response: completionBlock) }, socketDelegateQueue!) return tlsOptions } func open() { if (disconnected == false || disconnecting) { return } createDispatchQueues() let serverAddress = config.serverAddress let serverPort = config.serverPort let connection = NWConnection(host: NWEndpoint.Host(stringLiteral: serverAddress), port: NWEndpoint.Port(integerLiteral: serverPort), using: constructedParameters) connection.stateUpdateHandler = statusUpdateHandler self.connection = connection delegate?.connection(self, willConnectTo: serverAddress, on: serverPort) connect() } fileprivate func connect() { connecting = true connection?.start(queue: socketDelegateQueue!) } func close() { if (disconnected || disconnecting) { return } disconnecting = true connection?.cancel() } fileprivate func close(with error: NWError) { close(with: translateError(error)) } override func resetState() { super.resetState() connection = nil destroyDispatchQueues() } // MARK: - Socket Read & Write func read() { if (connected == false || disconnecting) { return } connection?.receive(minimumIncompleteLength: 0, maximumLength: maximumDataLength, completion: readCompletionHandler) } func readIn(_ data: Data) { if (disconnected || disconnecting) { return } /* First combine the existing read buffer with the new data so we can process it in mass. */ /* Note: June 29, 2018 on Swift 4.2 on Xcode 10 beta 2 When I first wrote this code, I wrote the logic in the form "newBuffer = (oldBuffer + data)" When writing it using this syntax, Foundation would throw at random an out of range exception similar to the following: *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSConcreteMutableData subdataWithRange:]: range {12945, 87} exceeds data length 7282' TODO: Maybe this should be revisited at a later time. */ var newBuffer: Data if let oldBuffer = readInBuffer { newBuffer = oldBuffer } else { newBuffer = Data() } newBuffer.append(data) /* Regardless of the result, we need to update the saved buffer with the updated buffer, but we prefer to wait until then end to do that. */ defer { readInBuffer = newBuffer } /* Split the data */ guard let (lines, remainingData) = newBuffer.splitNetworkLines() else { return } for line in lines { delegate?.connection(self, received: line) } if let remainder = remainingData { newBuffer = remainder } else { /* "Pass true to request that the collection avoid releasing its storage. Retaining the collection’s storage can be a useful optimization when you’re planning to grow the collection again." */ newBuffer.removeAll(keepingCapacity: true) } } func write(_ data: Data) { if (connected == false || disconnecting) { return } /* We only allow one write a time */ if (sending) { return } sending = true delegate?.connection(self, willSend: data) connection?.send(content: data, completion: .contentProcessed(writeCompletionHandler)) } // MARK: - Properties fileprivate var connectedHost: String? { guard let endpoint = connection?.currentPath?.remoteEndpoint else { return nil } if case let .hostPort(host, _) = endpoint { switch host { case .name(let address, _): return address case .ipv4(let address): return address.rawValue.IPv4Address case .ipv6(let address): return address.rawValue.IPv6Address @unknown default: fatalError("Unexpected switch case") } } return nil } fileprivate func onConnect() { connecting = false connected = true read() delegate?.connection(self, didConnectTo: connectedHost) onSecured() } fileprivate func onSecured() { /* We call onSecured() regardless of other preconditions then only mark ourselves as secured if we have protocol information. */ guard let protocolType = tlsNegotiatedProtocol, let cipherSuite = tlsNegotiatedCipherSuite else { return } secured = true delegate?.connection(self, securedWith: protocolType, cipherSuite: cipherSuite) } fileprivate func onDisconnect(with error: Error?) { defer { resetState() } var errorPayload: ConnectionError? if let alternateError = alternateDisconnectError { errorPayload = alternateError } else if let nwError = error as? NWError { errorPayload = translateError(nwError) } if (errorPayload == nil) { delegate?.connectionDisconnected(self) } else { delegate?.connection(self, disconnectedWith: errorPayload!) } } // NWConnection Delegate final func readCompletionHandler(_ content: Data?, _ contentContext: NWConnection.ContentContext?, _ isComplete: Bool, _ error: NWError?) { if (disconnecting) { return } if let error = error { close(with: error) return } if (contentContext?.isFinal == true && isComplete) { EOFReceived = true delegate?.connectionClosedReadStream(self) return } if (content == nil) { close(with: "Unexpected condition: There is no data when there is no error") return } readIn(content!) read() } final func writeCompletionHandler(_ error: NWError?) { if (disconnecting) { return } sending = false if let error = error { close(with: error) return } delegate?.connectionDidSend(self) } final func statusUpdateHandler(_ status: NWConnection.State) { switch status { case .waiting(let error): close(with: error) case .ready: onConnect() case .cancelled: onDisconnect(with: nil) case .failed(let error): onDisconnect(with: error) default: break } } // MARK: - Security final func tlsVerifySecProtocol(_ trust: sec_trust_t, response: @escaping sec_protocol_verify_complete_t) { let trustRef = sec_trust_copy_ref(trust).takeUnretainedValue() self.trustRef = trustRef tlsVerify(trustRef) { (underlyingResponse) in response(underlyingResponse) } } var tlsNegotiatedProtocol: tls_protocol_version_t? { var protocolType: tls_protocol_version_t? accessTLSMetadata { (metadata) in protocolType = sec_protocol_metadata_get_negotiated_tls_protocol_version(metadata) } return protocolType } var tlsNegotiatedCipherSuite: tls_ciphersuite_t? { var cipherSuite: tls_ciphersuite_t? accessTLSMetadata { (metadata) in cipherSuite = sec_protocol_metadata_get_negotiated_tls_ciphersuite(metadata) } return cipherSuite } var tlsCertificateChainData: [Data]? { var certificateChain: [Data]? accessTLSTrustRef { (trustRef) in certificateChain = RCMSecureTransport.certificates(in: trustRef) } return certificateChain } var tlsPolicyName: String? { var policyName: String? accessTLSTrustRef { (trustRef) in policyName = RCMSecureTransport.policyName(in: trustRef) if policyName == nil { /* June 09, 2019 with 10.15 Beta (19A471t): Despite us having a trustRef, we do not have a policy name when connecting with modern sockets to an IP address. The IP address itself is what is being matched against the certificate name anyways so let's just return it from config. TODO: Revisit this in a later beta or GM. */ let serverAddress = config.serverAddress if serverAddress.isIPAddress { policyName = serverAddress } } // policyName } return policyName } fileprivate func accessTLSMetadata(with closure: (sec_protocol_metadata_t) -> Void) { guard let genericMetadata = connection?.metadata(definition: NWProtocolTLS.definition) else { return } guard let tlsMetadata = genericMetadata as? NWProtocolTLS.Metadata else { return } closure(tlsMetadata.securityProtocolMetadata) } fileprivate func accessTLSTrustRef(with closure: (SecTrust) -> Void) { if let trustRef = trustRef { closure(trustRef) } } var tlsLocalIdentity: sec_identity_t? { guard let clientCertificate = clientSideCertificate else { return nil } /* And I thought I wrote verbose names... */ return sec_identity_create_with_certificates(clientCertificate.identity, ([clientCertificate.certificate] as CFArray)) } // MARK: - Error Handling fileprivate func translateError(_ error: NWError) -> ConnectionError { switch error { case .dns(let errorCode): return ConnectionError(nwDNSError: errorCode) case .posix(let errorCode): return ConnectionError(nwPOSIXError: errorCode.rawValue) case .tls(let errorCode): return ConnectionError(nwTLSError: errorCode) @unknown default: fatalError("Unexpected switch case") } } } fileprivate extension ConnectionError { init (nwDNSError: DNSServiceErrorType) { let errorCode = Int(nwDNSError) let errorReason: String switch errorCode { case kDNSServiceErr_NoError: errorReason = "No error" case kDNSServiceErr_NoSuchName: errorReason = "No such name" case kDNSServiceErr_NoMemory: errorReason = "No memory" case kDNSServiceErr_BadParam: errorReason = "Bad parameter" case kDNSServiceErr_BadReference: errorReason = "Bad reference" case kDNSServiceErr_BadState: errorReason = "Bad state" case kDNSServiceErr_BadFlags: errorReason = "Bad flags" case kDNSServiceErr_Unsupported: errorReason = "Unsupported" case kDNSServiceErr_NotInitialized: errorReason = "Not initialized" case kDNSServiceErr_AlreadyRegistered: errorReason = "Already registered" case kDNSServiceErr_NameConflict: errorReason = "Name conflict" case kDNSServiceErr_Invalid: errorReason = "Invalid" case kDNSServiceErr_Firewall: errorReason = "Firewall" case kDNSServiceErr_Incompatible: /* client library incompatible with daemon */ errorReason = "Incompatible" case kDNSServiceErr_BadInterfaceIndex: errorReason = "Bad interface index" case kDNSServiceErr_Refused: errorReason = "Refused" case kDNSServiceErr_NoSuchRecord: errorReason = "No such record" case kDNSServiceErr_NoAuth: errorReason = "No authentication" case kDNSServiceErr_NoSuchKey: errorReason = "No such key" case kDNSServiceErr_NATTraversal: errorReason = "NAT traversal" case kDNSServiceErr_DoubleNAT: errorReason = "Double NAT" case kDNSServiceErr_BadTime: /* Codes up to here existed in Tiger */ errorReason = "Bad time" case kDNSServiceErr_BadSig: errorReason = "Bad signature" case kDNSServiceErr_BadKey: errorReason = "Bad key" case kDNSServiceErr_Transient: errorReason = "Transient" case kDNSServiceErr_ServiceNotRunning: /* Background daemon not running */ errorReason = "Service not running" case kDNSServiceErr_NATPortMappingUnsupported: /* NAT doesn't support PCP, NAT-PMP or UPnP */ errorReason = "NAT port mapping unsupported" case kDNSServiceErr_NATPortMappingDisabled: /* NAT supports PCP, NAT-PMP or UPnP, but it's disabled by the administrator */ errorReason = "NAT port mapping disabled" case kDNSServiceErr_NoRouter: /* No router currently configured (probably no network connectivity) */ errorReason = "No router" case kDNSServiceErr_PollingMode: errorReason = "Polling mode" case kDNSServiceErr_Timeout: errorReason = "Timeout" default: errorReason = "Unknown" } let errorMessage = LocalizedString("DNS Error: %@ (%ld)", errorReason, errorCode, table: "ConnectionErrors") let nsError = NSError(domain: "NWErrorDomainDNS", code: errorCode, userInfo: [ NSLocalizedDescriptionKey : errorMessage ]) self.init(socketError: nsError) } init (nwPOSIXError: Int32) { let errorCode = Int(nwPOSIXError) let errorReason: String if let errorReasonC = strerror(nwPOSIXError) { errorReason = String(cString: errorReasonC) } else { errorReason = "Unknown" } let errorMessage = LocalizedString("POSIX Error: %@ (%ld)", errorReason, errorCode, table: "ConnectionErrors") let nsError = NSError(domain: "NWErrorDomainPOSIX", code: errorCode, userInfo: [ NSLocalizedDescriptionKey : errorMessage ]) self.init(socketError: nsError) } init (nwTLSError: OSStatus) { let errorCode = Int(nwTLSError) self.init(tlsError: errorCode) } } #endif ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Service/RCMProcessDelegate.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @implementation RCMProcessDelegate #pragma mark - #pragma mark XPC Delegate - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { NSXPCInterface *exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(RCMConnectionManagerServerProtocol)]; newConnection.exportedInterface = exportedInterface; RCMProcessMain *exportedObject = [[RCMProcessMain alloc] initWithXPCConnection:newConnection]; newConnection.exportedObject = exportedObject; NSXPCInterface *remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(RCMConnectionManagerClientProtocol)]; newConnection.remoteObjectInterface = remoteObjectInterface; [newConnection resume]; return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Service/RCMProcessMain.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface RCMProcessMain () @property (nonatomic, strong) IRCConnection *connection; @property (nonatomic, strong) NSXPCConnection *serviceConnection; @end @implementation RCMProcessMain - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithXPCConnection:(NSXPCConnection *)connection { NSParameterAssert(connection != nil); if ((self = [super init])) { self.serviceConnection = connection; LogToConsoleSetDefaultSubsystemToMainBundle(@"General"); return self; } return nil; } #pragma mark - #pragma mark XPC Interface - (void)openWithConfig:(IRCConnectionConfig *)config { NSAssert((self.connection == nil), @"Method invoked with connection already open"); IRCConnection *connection = [[IRCConnection alloc] initWithConfig:config onConnection:self.serviceConnection]; [connection open]; self.connection = connection; } - (void)close { NSAssert((self.connection != nil), @"Method invoked without performing setup first"); [self.connection close]; } - (void)sendData:(NSData *)data { [self sendData:data bypassQueue:NO]; } - (void)sendData:(NSData *)data bypassQueue:(BOOL)bypassQueue { NSAssert((self.connection != nil), @"Method invoked without performing setup first"); [self.connection sendData:data bypassQueue:bypassQueue]; } - (void)exportSecureConnectionInformation:(NS_NOESCAPE RCMSecureConnectionInformationCompletionBlock)completionBlock { NSAssert((self.connection != nil), @"Method invoked without performing setup first"); [self.connection exportSecureConnectionInformation:completionBlock error:NULL]; } - (void)enforceFloodControl { NSAssert((self.connection != nil), @"Method invoked without performing setup first"); [self.connection enforceFloodControl]; } - (void)clearSendQueue { NSAssert((self.connection != nil), @"Method invoked without performing setup first"); [self.connection clearSendQueue]; } - (void)enableAppNap { [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"NSAppSleepDisabled" : @(NO)}]; } - (void)disableAppNap { [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"NSAppSleepDisabled" : @(YES)}]; } - (void)enableSuddenTermination { [[NSProcessInfo processInfo] enableSuddenTermination]; } - (void)disableSuddenTermination { [[NSProcessInfo processInfo] disableSuddenTermination]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/IRC Remote Connection Manager/Classes/Service/main.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnullability-inferred-on-nested-type" int main(int argc, const char *argv[]) { RCMProcessDelegate *delegate = [RCMProcessDelegate new]; NSXPCListener *listener = [NSXPCListener serviceListener]; listener.delegate = delegate; [listener resume]; return 0; } #pragma clang diagnostic pop NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/IRC Remote Connection Manager/Configurations/Sandbox/Release.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.client ================================================ FILE: XPC Services/IRC Remote Connection Manager/IRC Remote Connection Manager.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C06DE7E20EC48560055D09A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAC2A071F12AFFB00D06046 /* Security.framework */; }; 4C06DE7F20EC48560055D09A /* SecurityInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAC2A081F12AFFB00D06046 /* SecurityInterface.framework */; }; 4C06DE8020EC48570055D09A /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CAC2A0B1F12B01700D06046 /* SystemConfiguration.framework */; }; 4C46A06920EC68B800094EA4 /* NSObjectHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A05720EC67E300094EA4 /* NSObjectHelper.m */; }; 4C46A06A20EC68B800094EA4 /* IRCConnectionConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A02B20EC67E300094EA4 /* IRCConnectionConfig.m */; }; 4C46A06B20EC68B800094EA4 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A02F20EC67E300094EA4 /* GCDAsyncSocket.m */; settings = {COMPILER_FLAGS = "-Wno-implicit-retain-self"; }; }; 4C46A06C20EC68B800094EA4 /* GCDAsyncSocketExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A03120EC67E300094EA4 /* GCDAsyncSocketExtensions.m */; }; 4C46A06D20EC68B800094EA4 /* TLOTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A03220EC67E300094EA4 /* TLOTimer.m */; }; 4C46A07020EC68B800094EA4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A00C20EC626E00094EA4 /* main.m */; }; 4C46A07120EC68B800094EA4 /* RCMProcessDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A00A20EC626E00094EA4 /* RCMProcessDelegate.m */; }; 4C46A07220EC68B800094EA4 /* RCMProcessMain.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A00B20EC626E00094EA4 /* RCMProcessMain.m */; }; 4C46A07320EC68C000094EA4 /* IRCConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C469FFF20EC626E00094EA4 /* IRCConnection.swift */; }; 4C46A07420EC68C000094EA4 /* IRCConnectionSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C46A00020EC626E00094EA4 /* IRCConnectionSocket.swift */; }; 4C46A07520EC68C000094EA4 /* IRCConnectionSocketClassic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C469FFE20EC626E00094EA4 /* IRCConnectionSocketClassic.swift */; }; 4C46A07620EC68C000094EA4 /* IRCConnectionSocketNWF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C469FFD20EC626E00094EA4 /* IRCConnectionSocketNWF.swift */; }; 4C5274DC20F7D49000B18F9D /* ConnectionErrors.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C5274DA20F7D49000B18F9D /* ConnectionErrors.strings */; }; 4C5274DE20F8D50C00B18F9D /* IRCConnectionErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C5274DD20F8D50C00B18F9D /* IRCConnectionErrors.m */; }; 4CACDB5820F747710075AFB5 /* TLOLocalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CACDB5720F747710075AFB5 /* TLOLocalization.m */; }; 4CACDB5B20F7496D0075AFB5 /* TLOLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACDB5A20F7496D0075AFB5 /* TLOLocalization.swift */; }; 4CE144EB20EC04F1000E01C9 /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8C72D21F1208F200ED72DB /* CocoaExtensions.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C1406421F11942B00C73D53 /* IRC Connection Host.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = "IRC Connection Host.xpc"; sourceTree = BUILT_PRODUCTS_DIR; }; 4C469FFD20EC626E00094EA4 /* IRCConnectionSocketNWF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IRCConnectionSocketNWF.swift; sourceTree = ""; }; 4C469FFE20EC626E00094EA4 /* IRCConnectionSocketClassic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IRCConnectionSocketClassic.swift; sourceTree = ""; }; 4C469FFF20EC626E00094EA4 /* IRCConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IRCConnection.swift; sourceTree = ""; }; 4C46A00020EC626E00094EA4 /* IRCConnectionSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IRCConnectionSocket.swift; sourceTree = ""; }; 4C46A00320EC626E00094EA4 /* RCMProcessMainPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCMProcessMainPrivate.h; sourceTree = ""; }; 4C46A00420EC626E00094EA4 /* RCMConnectionManagerProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCMConnectionManagerProtocol.h; sourceTree = ""; }; 4C46A00520EC626E00094EA4 /* SwiftBridgingHeaderPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgingHeaderPrivate.h; sourceTree = ""; }; 4C46A00620EC626E00094EA4 /* RCMProcessDelegatePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCMProcessDelegatePrivate.h; sourceTree = ""; }; 4C46A00720EC626E00094EA4 /* RCMProcessPCHPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCMProcessPCHPrivate.h; sourceTree = ""; }; 4C46A00820EC626E00094EA4 /* IRCConnectionPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCConnectionPrivate.h; sourceTree = ""; }; 4C46A00A20EC626E00094EA4 /* RCMProcessDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCMProcessDelegate.m; sourceTree = ""; }; 4C46A00B20EC626E00094EA4 /* RCMProcessMain.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCMProcessMain.m; sourceTree = ""; }; 4C46A00C20EC626E00094EA4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4C46A02220EC677B00094EA4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C46A02B20EC67E300094EA4 /* IRCConnectionConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IRCConnectionConfig.m; sourceTree = ""; }; 4C46A02F20EC67E300094EA4 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = ""; }; 4C46A03120EC67E300094EA4 /* GCDAsyncSocketExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocketExtensions.m; sourceTree = ""; }; 4C46A03220EC67E300094EA4 /* TLOTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TLOTimer.m; sourceTree = ""; }; 4C46A03720EC67E300094EA4 /* GCDAsyncSocketExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocketExtensions.h; sourceTree = ""; }; 4C46A03820EC67E300094EA4 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = ""; }; 4C46A03C20EC67E300094EA4 /* IRCConnectionConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IRCConnectionConfig.h; sourceTree = ""; }; 4C46A03F20EC67E300094EA4 /* IRCConnectionConfigInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IRCConnectionConfigInternal.h; sourceTree = ""; }; 4C46A04620EC67E300094EA4 /* NSObjectHelperPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSObjectHelperPrivate.h; sourceTree = ""; }; 4C46A04B20EC67E300094EA4 /* TLOTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TLOTimer.h; sourceTree = ""; }; 4C46A05720EC67E300094EA4 /* NSObjectHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSObjectHelper.m; sourceTree = ""; }; 4C46A06720EC687500094EA4 /* RCMConnectionManagerProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCMConnectionManagerProtocol.h; path = Classes/Headers/Private/RCMConnectionManagerProtocol.h; sourceTree = SOURCE_ROOT; }; 4C5274DB20F7D49000B18F9D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ConnectionErrors.strings; sourceTree = ""; }; 4C5274DD20F8D50C00B18F9D /* IRCConnectionErrors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IRCConnectionErrors.m; sourceTree = ""; }; 4C5274DF20F8D52C00B18F9D /* IRCConnectionErrors.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IRCConnectionErrors.h; sourceTree = ""; }; 4C8C72D21F1208F200ED72DB /* CocoaExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaExtensions.framework; path = "../../.tmp/SharedBuildProducts-Frameworks/CocoaExtensions.framework"; sourceTree = SOURCE_ROOT; }; 4CAC2A071F12AFFB00D06046 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 4CAC2A081F12AFFB00D06046 /* SecurityInterface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SecurityInterface.framework; path = System/Library/Frameworks/SecurityInterface.framework; sourceTree = SDKROOT; }; 4CAC2A0B1F12B01700D06046 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 4CACDB5720F747710075AFB5 /* TLOLocalization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TLOLocalization.m; sourceTree = ""; }; 4CACDB5920F747850075AFB5 /* TLOLocalization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TLOLocalization.h; sourceTree = ""; }; 4CACDB5A20F7496D0075AFB5 /* TLOLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLOLocalization.swift; sourceTree = ""; }; 4CE143F920EBF5DB000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE143FC20EBF5DB000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE143FE20EBF5DB000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE1440C20EBF5DB000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1440D20EBF5DB000E01C9 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4CE1441020EBF5DB000E01C9 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4CE1441120EBF5DB000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE1441320EBF5DB000E01C9 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4CE1442120EBF5DB000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1442420EBF5DB000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE1442620EBF5DB000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE1445520EBF641000E01C9 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; }; 4CE1448120EC0209000E01C9 /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 4C14063F1F11942B00C73D53 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4CE144EB20EC04F1000E01C9 /* CocoaExtensions.framework in Frameworks */, 4C06DE7E20EC48560055D09A /* Security.framework in Frameworks */, 4C06DE7F20EC48560055D09A /* SecurityInterface.framework in Frameworks */, 4C06DE8020EC48570055D09A /* SystemConfiguration.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4C1406391F11942B00C73D53 = { isa = PBXGroup; children = ( 4C469FFB20EC626E00094EA4 /* Classes */, 4C1406521F1194E900C73D53 /* Resources */, 4C8C72D71F1208F200ED72DB /* Frameworks */, 4C1406431F11942B00C73D53 /* Products */, ); sourceTree = ""; }; 4C1406431F11942B00C73D53 /* Products */ = { isa = PBXGroup; children = ( 4C1406421F11942B00C73D53 /* IRC Connection Host.xpc */, ); name = Products; sourceTree = ""; }; 4C1406521F1194E900C73D53 /* Resources */ = { isa = PBXGroup; children = ( 4C1406531F1194F100C73D53 /* Build Settings */, 4C46A01C20EC677B00094EA4 /* Language Files */, 4C46A02120EC677B00094EA4 /* Property Lists */, ); path = Resources; sourceTree = ""; }; 4C1406531F1194F100C73D53 /* Build Settings */ = { isa = PBXGroup; children = ( 4C1406541F1194F900C73D53 /* Configurations */, ); name = "Build Settings"; sourceTree = ""; }; 4C1406541F1194F900C73D53 /* Configurations */ = { isa = PBXGroup; children = ( 4CE143F520EBF5DB000E01C9 /* Build */, 4CE1442720EBF5DB000E01C9 /* Sandbox */, ); name = Configurations; path = ../../Configurations; sourceTree = SOURCE_ROOT; }; 4C469FFB20EC626E00094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C46A02620EC67E300094EA4 /* Shared */, 4C46A00120EC626E00094EA4 /* Headers */, 4C469FFC20EC626E00094EA4 /* IRC */, 4C46A00920EC626E00094EA4 /* Service */, ); path = Classes; sourceTree = ""; }; 4C469FFC20EC626E00094EA4 /* IRC */ = { isa = PBXGroup; children = ( 4C469FFF20EC626E00094EA4 /* IRCConnection.swift */, 4C46A00020EC626E00094EA4 /* IRCConnectionSocket.swift */, 4C469FFE20EC626E00094EA4 /* IRCConnectionSocketClassic.swift */, 4C469FFD20EC626E00094EA4 /* IRCConnectionSocketNWF.swift */, ); path = IRC; sourceTree = ""; }; 4C46A00120EC626E00094EA4 /* Headers */ = { isa = PBXGroup; children = ( 4C46A00220EC626E00094EA4 /* Private */, ); path = Headers; sourceTree = ""; }; 4C46A00220EC626E00094EA4 /* Private */ = { isa = PBXGroup; children = ( 4C46A00820EC626E00094EA4 /* IRCConnectionPrivate.h */, 4C46A00420EC626E00094EA4 /* RCMConnectionManagerProtocol.h */, 4C46A00620EC626E00094EA4 /* RCMProcessDelegatePrivate.h */, 4C46A00320EC626E00094EA4 /* RCMProcessMainPrivate.h */, 4C46A00720EC626E00094EA4 /* RCMProcessPCHPrivate.h */, 4C46A00520EC626E00094EA4 /* SwiftBridgingHeaderPrivate.h */, ); path = Private; sourceTree = ""; }; 4C46A00920EC626E00094EA4 /* Service */ = { isa = PBXGroup; children = ( 4C46A00C20EC626E00094EA4 /* main.m */, 4C46A00A20EC626E00094EA4 /* RCMProcessDelegate.m */, 4C46A00B20EC626E00094EA4 /* RCMProcessMain.m */, ); path = Service; sourceTree = ""; }; 4C46A01C20EC677B00094EA4 /* Language Files */ = { isa = PBXGroup; children = ( 4C5274DA20F7D49000B18F9D /* ConnectionErrors.strings */, ); path = "Language Files"; sourceTree = ""; }; 4C46A02120EC677B00094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C46A02220EC677B00094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C46A02620EC67E300094EA4 /* Shared */ = { isa = PBXGroup; children = ( 4C46A03520EC67E300094EA4 /* Headers */, 4C46A05520EC67E300094EA4 /* Helpers */, 4C46A02A20EC67E300094EA4 /* IRC */, 4C46A02C20EC67E300094EA4 /* Library */, ); name = Shared; path = ../../Sources/Shared; sourceTree = SOURCE_ROOT; }; 4C46A02A20EC67E300094EA4 /* IRC */ = { isa = PBXGroup; children = ( 4C46A02B20EC67E300094EA4 /* IRCConnectionConfig.m */, 4C5274DD20F8D50C00B18F9D /* IRCConnectionErrors.m */, ); path = IRC; sourceTree = ""; }; 4C46A02C20EC67E300094EA4 /* Library */ = { isa = PBXGroup; children = ( 4C46A02D20EC67E300094EA4 /* External Libraries */, 4CACDB5720F747710075AFB5 /* TLOLocalization.m */, 4CACDB5A20F7496D0075AFB5 /* TLOLocalization.swift */, 4C46A03220EC67E300094EA4 /* TLOTimer.m */, ); path = Library; sourceTree = ""; }; 4C46A02D20EC67E300094EA4 /* External Libraries */ = { isa = PBXGroup; children = ( 4C46A02E20EC67E300094EA4 /* Sockets */, ); path = "External Libraries"; sourceTree = ""; }; 4C46A02E20EC67E300094EA4 /* Sockets */ = { isa = PBXGroup; children = ( 4C46A02F20EC67E300094EA4 /* GCDAsyncSocket.m */, 4C46A03120EC67E300094EA4 /* GCDAsyncSocketExtensions.m */, ); path = Sockets; sourceTree = ""; }; 4C46A03520EC67E300094EA4 /* Headers */ = { isa = PBXGroup; children = ( 4C46A03620EC67E300094EA4 /* External Libraries */, 4C46A03D20EC67E300094EA4 /* Internal */, 4C46A04320EC67E300094EA4 /* Private */, 4C46A04C20EC67E300094EA4 /* Services */, 4C46A03C20EC67E300094EA4 /* IRCConnectionConfig.h */, 4C5274DF20F8D52C00B18F9D /* IRCConnectionErrors.h */, 4CACDB5920F747850075AFB5 /* TLOLocalization.h */, 4C46A04B20EC67E300094EA4 /* TLOTimer.h */, ); path = Headers; sourceTree = ""; }; 4C46A03620EC67E300094EA4 /* External Libraries */ = { isa = PBXGroup; children = ( 4C46A03820EC67E300094EA4 /* GCDAsyncSocket.h */, 4C46A03720EC67E300094EA4 /* GCDAsyncSocketExtensions.h */, ); path = "External Libraries"; sourceTree = ""; }; 4C46A03D20EC67E300094EA4 /* Internal */ = { isa = PBXGroup; children = ( 4C46A03F20EC67E300094EA4 /* IRCConnectionConfigInternal.h */, ); path = Internal; sourceTree = ""; }; 4C46A04320EC67E300094EA4 /* Private */ = { isa = PBXGroup; children = ( 4C46A04620EC67E300094EA4 /* NSObjectHelperPrivate.h */, ); path = Private; sourceTree = ""; }; 4C46A04C20EC67E300094EA4 /* Services */ = { isa = PBXGroup; children = ( 4C46A06720EC687500094EA4 /* RCMConnectionManagerProtocol.h */, ); path = Services; sourceTree = ""; }; 4C46A05520EC67E300094EA4 /* Helpers */ = { isa = PBXGroup; children = ( 4C46A05620EC67E300094EA4 /* Cocoa (Objective-C) */, ); path = Helpers; sourceTree = ""; }; 4C46A05620EC67E300094EA4 /* Cocoa (Objective-C) */ = { isa = PBXGroup; children = ( 4C46A05720EC67E300094EA4 /* NSObjectHelper.m */, ); path = "Cocoa (Objective-C)"; sourceTree = ""; }; 4C8C72D31F1208F200ED72DB /* External Frameworks */ = { isa = PBXGroup; children = ( 4C8C72D21F1208F200ED72DB /* CocoaExtensions.framework */, ); name = "External Frameworks"; sourceTree = ""; }; 4C8C72D61F1208F200ED72DB /* System Frameworks */ = { isa = PBXGroup; children = ( 4CE1445520EBF641000E01C9 /* Network.framework */, 4CAC2A071F12AFFB00D06046 /* Security.framework */, 4CAC2A081F12AFFB00D06046 /* SecurityInterface.framework */, 4CAC2A0B1F12B01700D06046 /* SystemConfiguration.framework */, ); name = "System Frameworks"; sourceTree = ""; }; 4C8C72D71F1208F200ED72DB /* Frameworks */ = { isa = PBXGroup; children = ( 4C8C72D31F1208F200ED72DB /* External Frameworks */, 4C8C72D61F1208F200ED72DB /* System Frameworks */, ); name = Frameworks; sourceTree = ""; }; 4CE143F520EBF5DB000E01C9 /* Build */ = { isa = PBXGroup; children = ( 4CE1440920EBF5DB000E01C9 /* Common */, 4CE1441E20EBF5DB000E01C9 /* Debug */, 4CE143F620EBF5DB000E01C9 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4CE143F620EBF5DB000E01C9 /* Standard Release */ = { isa = PBXGroup; children = ( 4CE143FE20EBF5DB000E01C9 /* Enabled Features.xcconfig */, 4CE143F920EBF5DB000E01C9 /* Textual.xcconfig */, 4CE143FC20EBF5DB000E01C9 /* XPC Services.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4CE1440920EBF5DB000E01C9 /* Common */ = { isa = PBXGroup; children = ( 4CE1441320EBF5DB000E01C9 /* Foundation Debug.xcconfig */, 4CE1441020EBF5DB000E01C9 /* Foundation.xcconfig */, 4CE1440D20EBF5DB000E01C9 /* Preserve Symbols.xcconfig */, 4CE1440C20EBF5DB000E01C9 /* Textual.xcconfig */, 4CE1441120EBF5DB000E01C9 /* XPC Services.xcconfig */, ); path = Common; sourceTree = ""; }; 4CE1441E20EBF5DB000E01C9 /* Debug */ = { isa = PBXGroup; children = ( 4CE1442620EBF5DB000E01C9 /* Enabled Features.xcconfig */, 4CE1442120EBF5DB000E01C9 /* Textual.xcconfig */, 4CE1442420EBF5DB000E01C9 /* XPC Services.xcconfig */, ); path = Debug; sourceTree = ""; }; 4CE1442720EBF5DB000E01C9 /* Sandbox */ = { isa = PBXGroup; children = ( 4CE1448120EC0209000E01C9 /* Release.entitlements */, ); name = Sandbox; path = Configurations/Sandbox; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 4C1406411F11942B00C73D53 /* IRC Remote Connection Manager */ = { isa = PBXNativeTarget; buildConfigurationList = 4C14064E1F11942B00C73D53 /* Build configuration list for PBXNativeTarget "IRC Remote Connection Manager" */; buildPhases = ( 4C46A02320EC678B00094EA4 /* Resources */, 4C14063F1F11942B00C73D53 /* Frameworks */, 4C14063E1F11942B00C73D53 /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "IRC Remote Connection Manager"; productName = "Connection Manager"; productReference = 4C1406421F11942B00C73D53 /* IRC Connection Host.xpc */; productType = "com.apple.product-type.xpc-service"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 4C14063A1F11942B00C73D53 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Codeux Software, LLC"; TargetAttributes = { 4C1406411F11942B00C73D53 = { CreatedOnToolsVersion = 8.0; LastSwiftMigration = 1100; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 4C14063D1F11942B00C73D53 /* Build configuration list for PBXProject "IRC Remote Connection Manager" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 4C1406391F11942B00C73D53; productRefGroup = 4C1406431F11942B00C73D53 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4C1406411F11942B00C73D53 /* IRC Remote Connection Manager */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4C46A02320EC678B00094EA4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C5274DC20F7D49000B18F9D /* ConnectionErrors.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 4C14063E1F11942B00C73D53 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C46A07320EC68C000094EA4 /* IRCConnection.swift in Sources */, 4CACDB5820F747710075AFB5 /* TLOLocalization.m in Sources */, 4C46A07420EC68C000094EA4 /* IRCConnectionSocket.swift in Sources */, 4C46A07520EC68C000094EA4 /* IRCConnectionSocketClassic.swift in Sources */, 4C46A07620EC68C000094EA4 /* IRCConnectionSocketNWF.swift in Sources */, 4C46A06920EC68B800094EA4 /* NSObjectHelper.m in Sources */, 4C46A06A20EC68B800094EA4 /* IRCConnectionConfig.m in Sources */, 4C46A06B20EC68B800094EA4 /* GCDAsyncSocket.m in Sources */, 4C46A06C20EC68B800094EA4 /* GCDAsyncSocketExtensions.m in Sources */, 4C5274DE20F8D50C00B18F9D /* IRCConnectionErrors.m in Sources */, 4C46A06D20EC68B800094EA4 /* TLOTimer.m in Sources */, 4CACDB5B20F7496D0075AFB5 /* TLOLocalization.swift in Sources */, 4C46A07020EC68B800094EA4 /* main.m in Sources */, 4C46A07120EC68B800094EA4 /* RCMProcessDelegate.m in Sources */, 4C46A07220EC68B800094EA4 /* RCMProcessMain.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 4C5274DA20F7D49000B18F9D /* ConnectionErrors.strings */ = { isa = PBXVariantGroup; children = ( 4C5274DB20F7D49000B18F9D /* en */, ); name = ConnectionErrors.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 4C14064C1F11942B00C73D53 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE1442420EBF5DB000E01C9 /* XPC Services.xcconfig */; buildSettings = { SWIFT_OBJC_BRIDGING_HEADER = "${PROJECT_DIR}/Classes/Headers/Private/SwiftBridgingHeaderPrivate.h"; }; name = Debug; }; 4C14064D1F11942B00C73D53 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE143FC20EBF5DB000E01C9 /* XPC Services.xcconfig */; buildSettings = { SWIFT_OBJC_BRIDGING_HEADER = "${PROJECT_DIR}/Classes/Headers/Private/SwiftBridgingHeaderPrivate.h"; }; name = Release; }; 4C14064F1F11942B00C73D53 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_ENTITLEMENTS = "${PROJECT_DIR}/Configurations/Sandbox/Release.entitlements"; DEVELOPMENT_TEAM = ""; GCC_PREFIX_HEADER = "${PROJECT_DIR}/Classes/Headers/Private/RCMProcessPCHPrivate.h"; INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-utilities.Textual-IRCConnectionHost"; PRODUCT_NAME = "IRC Connection Host"; }; name = Debug; }; 4C1406501F11942B00C73D53 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_ENTITLEMENTS = "${PROJECT_DIR}/Configurations/Sandbox/Release.entitlements"; DEVELOPMENT_TEAM = ""; GCC_PREFIX_HEADER = "${PROJECT_DIR}/Classes/Headers/Private/RCMProcessPCHPrivate.h"; INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-utilities.Textual-IRCConnectionHost"; PRODUCT_NAME = "IRC Connection Host"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 4C14063D1F11942B00C73D53 /* Build configuration list for PBXProject "IRC Remote Connection Manager" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C14064C1F11942B00C73D53 /* Debug */, 4C14064D1F11942B00C73D53 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4C14064E1F11942B00C73D53 /* Build configuration list for PBXNativeTarget "IRC Remote Connection Manager" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C14064F1F11942B00C73D53 /* Debug */, 4C1406501F11942B00C73D53 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 4C14063A1F11942B00C73D53 /* Project object */; } ================================================ FILE: XPC Services/IRC Remote Connection Manager/Resources/Language Files/en.lproj/ConnectionErrors.strings ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ "DNS Error: %@ (%ld)" = "DNS Error: %1$@ (%2$ld)"; "POSIX Error: %@ (%ld)" = "POSIX Error: %1$@ (%2$ld)"; ================================================ FILE: XPC Services/IRC Remote Connection Manager/Resources/Property Lists/Info.plist ================================================ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleDisplayName IRC Remote Connection Manager CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleVersion 1.0.0 NSHumanReadableCopyright Copyright © 2017 - 2020 Codeux Software, LLC. All rights reserved. XPCService JoinExistingSession ServiceType Application ================================================ FILE: XPC Services/Inline Content Loader/Build Scripts/BuildExtensions.sh ================================================ #!/bin/sh set -e ICL_PRODUCT_LOCATION="${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" ICL_PRODUCT_BINARY="${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}" # Core Media cd "${PROJECT_DIR}/Extensions/Core Media/" xcodebuild -target "Inline Content Loader Core Media" \ -configuration "${ICL_EXTENSION_BUILD_SCHEME}" \ ARCHS="${ARCHS}" \ CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}" \ DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" \ PROVISIONING_PROFILE_SPECIFIER="" \ TEXTUAL_WORKSPACE_DIR="${TEXTUAL_WORKSPACE_DIR}" \ TEXTUAL_PRODUCT_LOCATION="${TEXTUAL_PRODUCT_LOCATION}" \ ICL_PRODUCT_LOCATION="${ICL_PRODUCT_LOCATION}" \ ICL_PRODUCT_BINARY="${ICL_PRODUCT_BINARY}" exit 0; ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/ICLHelpers.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface ICLHelpers : NSObject + (nullable NSURL *)URLWithString:(NSString *)address; @end @interface ICLHelpers (JSON) + (NSURLSessionDataTask *)requestJSONDataFromURL:(NSURL *)url completionBlock:(void (^)(BOOL success, NSDictionary * _Nullable data))completionBlock; + (NSURLSessionDataTask *)requestJSONDataFromAddress:(NSString *)address completionBlock:(void (^)(BOOL success, NSDictionary * _Nullable data))completionBlock; + (NSURLSessionDataTask *)requestJSONObject:(NSString *)objectKey ofType:(Class)objectType inHierarchy:(nullable NSArray *)hierarchy fromURL:(NSURL *)url completionBlock:(void (^)(id _Nullable object))completionBlock; + (NSURLSessionDataTask *)requestJSONObject:(NSString *)objectKey ofType:(Class)objectType inHierarchy:(nullable NSArray *)hierarchy fromAddress:(NSString *)address completionBlock:(void (^)(id _Nullable object))completionBlock; @end @interface ICLHelpers (Errors) @property (copy, readonly, class) NSError *genericValidationFailedError; @end @interface NSString (ICLHelpers) /* Given "youtube.com" as input, returns if string is equal to "youtube.com" or has suffix ".youtube.com" */ - (BOOL)isDomain:(NSString *)domain; - (BOOL)isDomainOrSubdomain:(NSString *)domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/ICLInlineContentModule.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLPayload.h" #import "ICLMediaType.h" #import NS_ASSUME_NONNULL_BEGIN @class ICLInlineContentModule; /* See description for -actionBlockForURL: */ typedef void (^ICLInlineContentModuleActionBlock)(ICLInlineContentModule *module); /** * Modules are always a subclass, instead of a protocol, to give us * greater flexibility when adding private components to the parent. */ @interface ICLInlineContentModule : NSObject /** * Mutable copy of the payload this module has access to modify. * This payload includes the address and the unique identifier. */ @property (readonly, strong) ICLPayloadMutable *payload; /** * Module objects are not allowed to be allocated by a plugin. */ - (instancetype)init NS_UNAVAILABLE; #pragma mark - #pragma mark Rules /** An optional array of domains that the module is specific to. If this array is non-nil and the domain does not appear in the array, then the module is skipped over. One of the action methods defined below is never called. */ @property (readonly, copy, nullable, class) NSArray *domains; #pragma mark - #pragma mark Action /** * Returns a block to perform if the module is interested in the URL. * * The block is passed one argument: a new instance of the module. * The block can then use that to do stateful work, or it can * disregard the argument and do whatever else it wants. * * The return value of -actionBlockForURL: is favored over -actionForURL: * The latter is never called if -actionBlockForURL: returns a value. * * nil is returned if this method is not implemented. */ + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url; /** * Returns a selector to perform if the module is interested in the URL. * * The selector is performed on a new instance of the module which means * the selector returned must be one for an instance method. * * The selector: * 1. Does not return a value (void) * 2. Does not take arguments * * Example: * * - (void)performAction * { * * } * * NULL is returned if this method is not implemented. */ + (nullable SEL)actionForURL:(NSURL *)url; #pragma mark - #pragma mark Context /** Whether the module's content is an image or video. This can include video services, not just video files. */ @property (readonly, class) BOOL contentImageOrVideo; /** Whether the module's content is a regular file, such as an image or video. Not an iframe, embedded, or dynamic content (such as JavaScript). */ @property (readonly, class) BOOL contentIsFile; /** Whether the module might add content to the DOM which is not trusted such as HTML downloaded from some website. A module that loads remote JavaScript libraries is also considered untrusted. */ @property (readonly, class) BOOL contentUntrusted; /** Whether module might load content that is not safe for work. */ @property (readonly, class) BOOL contentNotSafeForWork; #pragma mark - #pragma mark Resources /* If a non-nil value is returned for any of these properties, then that value is inserted into the payload. */ /* See ICLPayload.h for a description of each property. */ @property (copy, readonly, nullable) NSArray *styleResources; @property (copy, readonly, nullable) NSArray *scriptResources; @property (copy, readonly, nullable) NSString *entrypoint; /** URL to a file that is a mustache template. Given a URL, the -template property automatically returns a reference to the template. It is possible to render HTML multiple ways which means you do not need a template unless you want one. */ @property (readonly, nullable) NSURL *templateURL; /** Reference to mustache template found at -templateURL */ @property (readonly, nullable) GRMustacheTemplate *template; @end #pragma mark - #pragma mark Completion @interface ICLInlineContentModule (Completion) /** Called by module to inform the media handler that no more modifications will be made to the payload and that the contents of it should now be processed. - Warnings This method will throw an exception if the module has already been finalized by calling this method or a sibling. */ - (void)finalize; /** Called by module to inform the media handler that no more modifications will be made to the payload and that the contents of it should now be processed. - Warnings This method will throw an exception if the module has already been finalized by calling this method or a sibling. - Arguments @param error nil to inform the media handler that the media should be inlined or an error describing why that cannot happen. */ - (void)finalizeWithError:(nullable NSError *)error; /** Called by the module to inform the media handler that the module wants to cancel performing any work. - Warnings This method will throw an exception if the module has already been finalized by calling this method or a sibling. */ - (void)cancel; /** A module that is capable of performing work on more than one type of media (such as images and videos) can call this method when it is sure what type of media it has. The media will be checked by performing a web request to ensure that it is in fact the type described. - See also See -deferAsType:performCheck: for a detailed description. */ - (void)deferAsType:(ICLMediaType)type; // performCheck = YES /** A module that is capable of performing work on more than one type of media (such as images and videos) can call this method when it is sure what type of media it has. - Examples For example, if the module has the URL "example.com" and believes it is a regular image, then it can call this method with the type ICLMediaTypeImage to inform the media handler to inline the URL as an image. - Notes Calling this method to inline images or other media types is only required if the module is not already a subclass of one of the root classes responsible for handling these types of media. - Warnings This method will throw an exception if the module has already been finalized by calling this method or a sibling. - Arguments @param type The type of media that the content should be treated as. The types supported are: • ICLMediaTypeImage for images, • ICLMediaTypeVideo for videos, • ICLMediaTypeVideoGif for videos presented as a gif @param performCheck Whether to perform a web request to ensure that it is in fact the type described. */ - (void)deferAsType:(ICLMediaType)type performCheck:(BOOL)performCheck; /** Returns YES if a type is supported by -deferAsType: */ + (BOOL)isTypeDeferrable:(ICLMediaType)type; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/ICLMediaAssessment.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLMediaType.h" NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Immutable Object @interface ICLMediaAssessment : XRPortablePropertyObject - (instancetype)init NS_UNAVAILABLE; /** Designated initializer */ - (instancetype)initWithURL:(NSURL *)url asType:(ICLMediaType)type NS_DESIGNATED_INITIALIZER; /** The final URL that was assessed. This URL may be different when compared to that passed to the assessor due to HTTP redirects. */ @property (readonly, copy) NSURL *url; /** The type of the media. The type is determined by the Content-Type header. */ @property (readonly) ICLMediaType type; /** Value of the Content-Type header */ @property (readonly, copy) NSString *contentType; /** Value of the Content-Length header */ @property (readonly) unsigned long long contentLength; @end #pragma mark - #pragma mark Mutable Object @interface ICLMediaAssessmentMutable : ICLMediaAssessment @property (nonatomic, assign, readwrite) ICLMediaType type; @property (nonatomic, copy, readwrite) NSString *contentType; // Defaults to "application/binary" @property (nonatomic, assign, readwrite) unsigned long long contentLength; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/ICLMediaAssessor.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLMediaType.h" #import "ICLMediaAssessment.h" /* Given a URL, the accessor will load the contents of that URL to determine what type of media it is: image, video, or other. It will also perform validations, based on user preference. For example, if the user doesn't want to see images larger than 300 pixels and the image is 400 pixels, then you get an error. */ NS_ASSUME_NONNULL_BEGIN /* Both values will never be nil together. There will either be an assessment or an error for why there isn't. */ /* It is possible for there to be an assessment AND an error. If that is the case, the assessor was able to determine the type of media, but it was unable to perform extended validation. Treat as failure. */ typedef void (^ICLMediaAssessorCompletionBlock)(ICLMediaAssessment * _Nullable assessment, NSError * _Nullable error); @interface ICLMediaAssessor : NSObject - (instancetype)init NS_UNAVAILABLE; /* Use the following two methods to determine what type of media a URL is. */ + (instancetype)assessorForURL:(NSURL *)url completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock; + (instancetype)assessorForAddress:(NSString *)address completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock; /* Use the following two methods to determine whether the URL is the type of media. */ /* If you are expecting the URL to be a specific type of media, these methods are better. */ + (instancetype)assessorForURL:(NSURL *)url withType:(ICLMediaType)type completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock; + (instancetype)assessorForAddress:(NSString *)address withType:(ICLMediaType)type completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock; /* Suspend assessment */ - (void)suspend; /* Resume assessment */ - (void)resume; /* Logging */ + (void)logError:(NSError *)error; @end /* Error codes */ typedef NS_ENUM(NSUInteger, ICLMediaAssessorErrorCode) { /* Catch all */ ICLMediaAssessorErrorCodeAssessmentFailed = 0, /* Endpoint did not respond with OK (200) */ ICLMediaAssessorErrorCodeUnexpectedStatusCode = 1001, /* Content-Type header is improperly formatted */ ICLMediaAssessorErrorCodeMalformedContentType = 1002, /* Content-Length header is improperly formatted */ ICLMediaAssessorErrorCodeMalformedContentLength = 1003, /* Unexpected media type */ ICLMediaAssessorErrorCodeUnexpectedType = 1004, /* Unexpected response type (not HTTP) */ ICLMediaAssessorErrorCodeUnexpectedResponse = 1005, /* Maximum response size exceeded */ ICLMediaAssessorErrorCodeContentLengthExceeded = 1006, /* Image validation: Maximum width exceeded */ ICLMediaAssessorErrorCodeMaximumWidthExceeded = 1007, /* Image validation: Maximum height exceeded */ ICLMediaAssessorErrorCodeMaximumHeightExceeded = 1008, }; NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/ICLMediaType.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, ICLMediaType) { ICLMediaTypeUnknown = 0, ICLMediaTypeImage, ICLMediaTypeVideo, ICLMediaTypeVideoGif, ICLMediaTypeOther }; NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/ICLPayload.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN #pragma mark - #pragma mark Immutable Object @interface ICLPayload : XRPortablePropertyObject /** Payload objects are not allowed to be allocated by a plugin. Each new instance of ICLContentLoaderModule is given a mutable payload. Modify that, then the loader will create an immutable copy when appropriate. */ - (instancetype)init NS_UNAVAILABLE; /** The URL associated with this payload. */ @property (copy, readonly) NSURL *url; /** String value of -url property. */ @property (copy, readonly) NSString *address; /** The URL of the content to inline, such as an image, can be different than the value of -url because of redirects or API requests. The -urlToInline property can be used to get and set this different URL. If left unchanged, the value of this property mirrors -url */ @property (copy, readonly) NSURL *urlToInline; /** String value of the -urlToInline property. */ @property (copy, readonly) NSString *addressToInline; /** The unique identifier associated with this payload. */ @property (copy, readonly) NSString *uniqueIdentifier; /** The view responsible for this payload. */ @property (copy, readonly) NSString *viewIdentifier; /** The line number associated with this payload. */ @property (copy, readonly) NSString *lineNumber; /** Position of payload in relation to others with same line number. */ @property (readonly) NSUInteger index; /** The length of the content. This value is optional. */ @property (readonly) unsigned long long contentLength; /** The size of the content. This value is optional. */ @property (readonly) NSSize contentSize; /** A collection of paths for .css files that need to be loaded to allow the rendered HTML to appear correct. */ @property (copy, readonly) NSArray *styleResources; /** A collection of paths for .js files that need to be loaded to allow the rendered HTML to appear correct. */ @property (copy, readonly) NSArray *scriptResources; /** Rendered HTML or an empty string - Discussion: A module does not need to render the HTML through Objective-C. It can render it in JavaScript or by some other means. If a module renders HTML using Objective-C, then the final result can be assigned to this property. */ @property (copy, readonly) NSString *html; #pragma mark - #pragma mark Advanced /** The name of a JavaSript object that a function named entrypoint() is called on for the purpose of inlining this payload. - Inheritance Textual offers a prototype named InlineMediaPrototype that your object can inherit from. This prototype already contains an entrypoint() function which you can override. - Function Arguments: The entrypoint function takes two arguments: 1. The value of the -entrypointPayload property defined below. 2. A callback function which itself takes one argument: The HTML that the entrypoint function wants to insert. - Requirements: • If the `html` property of this payload is empty: 1. An entrypoint function is REQUIRED. • If the `html` property of this payload is NOT empty: 1. An entrypoint function is OPTIONAL. 2. If an entrypoint function is NOT set, then the value of the `html` property is inserted without assitance. 3. If an entrypoin function is set, then the value of the contents of the payload are passed to it without inserting the value of the `html` property. The entrypoint function can then decide to insert the HTML when it wants by calling a callback function. - Example: ```` MyObject.entrypoint = function(payload, callbackFunction) { // Do work here... callbackFunction("some HTML to display"); } ```` */ @property (copy, readonly, nullable) NSString *entrypoint; /** A dictionary that is passed as the first argument to -entrypoint. - Dictionary Contents: This dictionary is guaranteed to always contain the following keys: 1. "html" (string) 2. "url" (string) 3. "urlToInline" (string) 4. "lineNumber" (string) 5. "uniqueIdentifier" (string) The value of these keys mirror the payload's. ICLPayload will not allow you to override the value of these keys. - Types: Types are translated as such: ```` Objective-C JavaScript ----------- ---------- NSArray => array BOOL => boolean NSNumber => number NSDictionary => object NSString => string NSURL => string ```` Custom types are treated as "undefined" */ @property (copy, readonly) NSDictionary> *entrypointPayload; /** An optional class that is appended to the inlined media. - Examples • inlineStreamable is used for Streamable, • inlineVimeo is used for Vimeo, • inlineYouTube is used for YouTube */ @property (copy, readonly) NSString *classAttribute; @end #pragma mark - #pragma mark Mutable Object @interface ICLPayloadMutable : ICLPayload @property (nonatomic, copy, readwrite) NSURL *urlToInline; @property (nonatomic, assign, readwrite) unsigned long long contentLength; @property (nonatomic, assign, readwrite) NSSize contentSize; @property (nonatomic, copy, readwrite) NSArray *styleResources; @property (nonatomic, copy, readwrite) NSArray *scriptResources; @property (nonatomic, copy, readwrite) NSString *html; @property (nonatomic, copy, nullable, readwrite) NSString *entrypoint; @property (nonatomic, copy, null_resettable, readwrite) NSDictionary> *entrypointPayload; @property (nonatomic, copy, readwrite) NSString *classAttribute; - (instancetype)init; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/ICLPluginProtocol.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "ICLInlineContentModule.h" NS_ASSUME_NONNULL_BEGIN /** The ICLPluginProtocol protocol defines methods and properties that a plugin can implement. */ @protocol ICLPluginProtocol @required /** An array of classes that are a subclass of ICLInlineContentModule It is recommended that classes inside a plugin use the prefix ICP so that their classes do not conflict with those that already exist. */ @property (readonly, copy, class) NSArray *modules; @end NS_ASSUME_NONNULL_END /* Import root classes a module can subclass (if they want) */ #import "ICMInlineHTML.h" #import "ICMInlineImage.h" #import "ICMInlineVideo.h" /* Import helpers that a plugin can access */ #import "ICLHelpers.h" #import "ICLMediaAssessor.h" ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Internal/ICLInlineContentModuleInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentModulePrivate.h" NS_ASSUME_NONNULL_BEGIN @interface ICLInlineContentModule () { @private ICLProcessMain *_process; ICLPayloadMutable *_payload; BOOL _moduleFinalized; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Internal/ICLMediaAssessmentInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface ICLMediaAssessment () { @protected NSURL *_url; ICLMediaType _type; NSString *_contentType; unsigned long long _contentLength; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Internal/ICLPayloadInternal.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLPayload.h" NS_ASSUME_NONNULL_BEGIN @interface ICLPayload () { @protected NSURL *_urlToInline; unsigned long long _contentLength; NSSize _contentSize; NSArray *_styleResources; NSArray *_scriptResources; NSString *_html; NSString *_entrypoint; NSDictionary> *_entrypointPayload; NSString *_classAttribute; @private NSURL *_url; NSString *_lineNumber; NSString *_uniqueIdentifier; NSString *_viewIdentifier; NSUInteger _index; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Private/CoreModulesImportsPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineHTML.h" #import "ICMInlineVideo.h" #import "ICMInlineImage.h" #import "ICMAssessedMedia.h" ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Private/ICLInlineContentModulePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentProtocol.h" #import "ICLInlineContentModule.h" NS_ASSUME_NONNULL_BEGIN @class GRMustacheTemplate, ICLPayloadMutable, ICLProcessMain; @interface ICLInlineContentModule () - (instancetype)initWithPayload:(ICLPayloadMutable *)payload inProcess:(ICLProcessMain *)process; - (instancetype)initWithDeferredModule:(ICLInlineContentModule *)module; @end @interface ICLInlineContentModule (CompletionPrivate) - (void)finalizePreflight; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Private/ICLInlineContentProtocol.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* *** XPC PROTOCOL HEADERS ARE PRIVATE *** */ NS_ASSUME_NONNULL_BEGIN @class ICLPayload; #pragma mark - #pragma mark Server Protocol @protocol ICLInlineContentServerProtocol @required - (void)warmServiceByLoadingPluginsAtLocations:(NSArray *)pluginLocations; - (void)warmServiceByRegisteringDefaults:(NSDictionary *)defaults; - (void)processURL:(NSURL *)url withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index inView:(NSString *)viewIdentifier; - (void)processPayload:(ICLPayload *)payload; @end #pragma mark - #pragma mark Client Protocol @protocol ICLInlineContentClientProtocol @required - (void)processingPayloadSucceeded:(ICLPayload *)payload; - (void)processingPayload:(ICLPayload *)payload failedWithError:(NSError *)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Private/ICLPayloadPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLPayload.h" NS_ASSUME_NONNULL_BEGIN @interface ICLPayload (ICLPayloadPrivate) - (nullable instancetype)initWithURL:(NSURL *)url withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index inView:(NSString *)viewIdentifier; - (instancetype)initWithDeferredPayload:(ICLPayload *)payload; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Private/ICLPluginManagerPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface ICLPluginManager : NSObject + (ICLPluginManager *)sharedPluginManager; - (void)loadPluginsAtLocations:(NSArray *)pluginLocations; @property (readonly, copy) NSArray *modules; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Private/ICLProcessDelegatePrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ NS_ASSUME_NONNULL_BEGIN @interface ICLProcessDelegate : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Private/ICLProcessMainPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentProtocol.h" #import "ICLMediaType.h" NS_ASSUME_NONNULL_BEGIN @class ICLInlineContentModule; @interface ICLProcessMain : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithXPCConnection:(NSXPCConnection *)connection NS_DESIGNATED_INITIALIZER; - (void)_finalizeModule:(ICLInlineContentModule *)module withError:(nullable NSError *)error; - (void)_cancelModule:(ICLInlineContentModule *)module; - (void)_deferModule:(ICLInlineContentModule *)module asType:(ICLMediaType)type performCheck:(BOOL)performCheck; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Headers/Private/ICLProcessPCHPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import #import "StaticDefinitions.h" extern NSErrorDomain const ICLInlineContentErrorDomain; ================================================ FILE: XPC Services/Inline Content Loader/Classes/Modules/Assessed Media/ICMAssessedMedia.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentModule.h" NS_ASSUME_NONNULL_BEGIN /* Checks every URL to determine if it is an image or video, then delegates responsibility for module to an instance of a root class. */ @interface ICMAssessedMedia : ICLInlineContentModule @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Modules/Assessed Media/ICMAssessedMedia.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferences.h" #import "ICLMediaAssessor.h" #import "ICMAssessedMedia.h" NS_ASSUME_NONNULL_BEGIN @interface ICMAssessedMedia () @property (nonatomic, strong, nullable) ICLMediaAssessor *mediaAssessor; @end @implementation ICMAssessedMedia - (void)_assessMedia { NSURL *url = self.payload.url; ICLMediaAssessor *mediaAssessor = [ICLMediaAssessor assessorForURL:url completionBlock:^(ICLMediaAssessment *assessment, NSError *error) { ICLMediaType type = ((assessment) ? assessment.type : ICLMediaTypeUnknown); BOOL safeToLoad = (assessment != nil && error == nil && [ICLInlineContentModule isTypeDeferrable:type]); if (safeToLoad) { [self _safeToLoadMediaOfType:type atURL:assessment.url]; } else { [self _unsafeToLoadMedia]; } self.mediaAssessor = nil; }]; self.mediaAssessor = mediaAssessor; [mediaAssessor resume]; } - (void)_unsafeToLoadMedia { [self cancel]; } - (void)_safeToLoadMediaOfType:(ICLMediaType)type atURL:(NSURL *)url { self.payload.urlToInline = url; [self deferAsType:type performCheck:NO]; } #pragma mark - #pragma mark Action + (nullable SEL)actionForURL:(NSURL *)url { if ([TPCPreferences inlineMediaCheckEverything] == NO) { return NULL; } return @selector(_assessMedia); } #pragma mark - #pragma mark Utilities + (BOOL)contentImageOrVideo { return YES; } + (BOOL)contentIsFile { return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Modules/Root Classes/ICMInlineHTML.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentModule.h" NS_ASSUME_NONNULL_BEGIN /* ICMInlineHTMLFoundation does nothing. It exists for internal use. */ @interface ICMInlineHTMLFoundation : ICLInlineContentModule @end /* Proper class to sublcass if that is your thing. */ @interface ICMInlineHTML : ICMInlineHTMLFoundation - (void)performActionForHTML:(NSString *)unescapedHTML; + (ICLInlineContentModuleActionBlock)actionBlockForHTML:(NSString *)unescapedHTML; - (void)notifyUnableToPresentHTML; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Modules/Root Classes/ICMInlineHTML.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineHTML.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMInlineHTML - (void)performActionForHTML:(NSString *)unescapedHTML { NSParameterAssert(unescapedHTML != nil); ICLPayloadMutable *payload = self.payload; NSDictionary *templateAttributes = @{ @"classAttribute" : payload.classAttribute, @"unescapedHTML" : unescapedHTML, @"uniqueIdentifier" : payload.uniqueIdentifier }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } - (void)notifyUnableToPresentHTML { [self cancel]; } #pragma mark - #pragma mark Action Block + (ICLInlineContentModuleActionBlock)actionBlockForHTML:(NSString *)html { NSParameterAssert(html != nil); return [^(ICLInlineContentModule *module) { __weak ICMInlineHTML *moduleTyped = (id)module; [moduleTyped performActionForHTML:html]; } copy]; } @end #pragma mark - #pragma mark Foundation @implementation ICMInlineHTMLFoundation - (nullable NSArray *)styleResources { static NSArray *styleResources = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ styleResources = @[ [RZMainBundle() URLForResource:@"ICMInlineHTML" withExtension:@"css" subdirectory:@"Components"] ]; }); return styleResources; } - (nullable NSArray *)scriptResources { static NSArray *scriptResources = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ scriptResources = @[ [RZMainBundle() URLForResource:@"ICMInlineHTML" withExtension:@"js" subdirectory:@"Components"] ]; }); return scriptResources; } - (nullable NSURL *)templateURL { return [RZMainBundle() URLForResource:@"ICMInlineHTML" withExtension:@"mustache" subdirectory:@"Components"]; } - (nullable NSString *)entrypoint { return @"_ICMInlineHTML"; } + (BOOL)contentUntrusted { return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Modules/Root Classes/ICMInlineImage.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentModule.h" NS_ASSUME_NONNULL_BEGIN /* ICMInlineImageFoundation does nothing. It exists for internal use. */ @interface ICMInlineImageFoundation : ICLInlineContentModule @end /* Proper class to sublcass if that is your thing. */ @interface ICMInlineImage : ICMInlineImageFoundation - (void)performAction; // checkImage = YES - (void)performActionWithImageCheck:(BOOL)checkImage; - (void)performActionForURL:(NSURL *)url; // bypassImageCheck = NO - (void)performActionForURL:(NSURL *)url bypassImageCheck:(BOOL)bypassImageCheck; - (void)performActionForAddress:(NSString *)address; // bypassImageCheck = NO - (void)performActionForAddress:(NSString *)address bypassImageCheck:(BOOL)bypassImageCheck; + (ICLInlineContentModuleActionBlock)actionBlockURL:(NSURL *)url; // bypassImageCheck = NO + (ICLInlineContentModuleActionBlock)actionBlockURL:(NSURL *)url bypassImageCheck:(BOOL)bypassImageCheck; + (ICLInlineContentModuleActionBlock)actionBlockForAddress:(NSString *)address; // bypassImageCheck = NO + (ICLInlineContentModuleActionBlock)actionBlockForAddress:(NSString *)address bypassImageCheck:(BOOL)bypassImageCheck; - (void)notifyUnsafeToLoadImage; @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Modules/Root Classes/ICMInlineImage.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferences.h" #import "ICLHelpers.h" #import "ICLMediaAssessor.h" #import "ICMInlineImage.h" NS_ASSUME_NONNULL_BEGIN @interface ICMInlineImage () @property (nonatomic, strong, nullable) ICLMediaAssessor *imageCheck; @end @implementation ICMInlineImage - (void)performAction { [self performActionWithImageCheck:YES]; } - (void)performActionWithImageCheck:(BOOL)checkImage { if (checkImage) { [self _performImageCheck]; } else { [self _safeToLoadImage]; } } - (void)performActionForURL:(NSURL *)url { [self performActionForURL:url bypassImageCheck:NO]; } - (void)performActionForURL:(NSURL *)url bypassImageCheck:(BOOL)bypassImageCheck { NSParameterAssert(url != nil); NSAssert((self.imageCheck == nil), @"Module already initialized"); self.payload.urlToInline = url; [self performActionWithImageCheck:(bypassImageCheck == NO)]; } - (void)performActionForAddress:(NSString *)address { [self performActionForAddress:address bypassImageCheck:NO]; } - (void)performActionForAddress:(NSString *)address bypassImageCheck:(BOOL)bypassImageCheck { NSParameterAssert(address != nil); NSURL *url = [ICLHelpers URLWithString:address]; [self performActionForURL:url bypassImageCheck:bypassImageCheck]; } - (void)_performImageCheck { ICLPayload *payload = self.payload; ICLMediaAssessor *imageCheck = [ICLMediaAssessor assessorForURL:payload.urlToInline withType:ICLMediaTypeImage completionBlock:^(ICLMediaAssessment *assessment, NSError *error) { BOOL safeToLoad = (error == nil); if (safeToLoad) { [self _safeToLoadImage]; } else { [self _unsafeToLoadImage]; [ICLMediaAssessor logError:error]; } self.imageCheck = nil; }]; self.imageCheck = imageCheck; [imageCheck resume]; } - (void)_unsafeToLoadImage { [self notifyUnsafeToLoadImage]; } - (void)_safeToLoadImage { ICLPayloadMutable *payload = self.payload; NSDictionary *templateAttributes = @{ @"anchorLink" : payload.address, @"classAttribute" : payload.classAttribute, @"imageURL" : payload.addressToInline, @"preferredMaximumWidth" : @([TPCPreferences inlineMediaMaxWidth]), @"uniqueIdentifier" : payload.uniqueIdentifier }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } - (void)notifyUnsafeToLoadImage { [self cancel]; } #pragma mark - #pragma mark Action Block + (ICLInlineContentModuleActionBlock)actionBlockURL:(NSURL *)url { return [self actionBlockURL:url bypassImageCheck:NO]; } + (ICLInlineContentModuleActionBlock)actionBlockURL:(NSURL *)url bypassImageCheck:(BOOL)bypassImageCheck { NSParameterAssert(url != nil); return [self actionBlockForAddress:url.absoluteString bypassImageCheck:bypassImageCheck]; } + (ICLInlineContentModuleActionBlock)actionBlockForAddress:(NSString *)address { return [self actionBlockForAddress:address bypassImageCheck:NO]; } + (ICLInlineContentModuleActionBlock)actionBlockForAddress:(NSString *)address bypassImageCheck:(BOOL)bypassImageCheck { NSParameterAssert(address != nil); return [^(ICLInlineContentModule *module) { __weak ICMInlineImage *moduleTyped = (id)module; [moduleTyped performActionForAddress:address bypassImageCheck:NO]; } copy]; } #pragma mark - #pragma mark Utilities + (NSArray *)validImageContentTypes { static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = @[@"image/gif", @"image/jpeg", @"image/png", @"image/svg+xml", @"image/tiff", @"image/x-ms-bmp"]; }); return cachedValue; } + (NSArray *)validVideoContentTypes { static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = @[@"video/3gpp", @"video/3gpp2", @"video/mp4", @"video/quicktime", @"video/x-m4v"]; }); return cachedValue; } @end #pragma mark - #pragma mark Foundation @implementation ICMInlineImageFoundation + (BOOL)contentImageOrVideo { return YES; } - (nullable NSURL *)templateURL { return [RZMainBundle() URLForResource:@"ICMInlineImage" withExtension:@"mustache" subdirectory:@"Components"]; } - (nullable NSArray *)styleResources { static NSArray *styleResources = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ styleResources = @[ [RZMainBundle() URLForResource:@"ICMInlineImage" withExtension:@"css" subdirectory:@"Components"] ]; }); return styleResources; } - (nullable NSArray *)scriptResources { static NSArray *scriptResources = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ scriptResources = @[ [RZMainBundle() URLForResource:@"InlineImageLiveResize" withExtension:@"js"], [RZMainBundle() URLForResource:@"ICMInlineImage" withExtension:@"js" subdirectory:@"Components"] ]; }); return scriptResources; } - (nullable NSString *)entrypoint { return @"_ICMInlineImage"; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Modules/Root Classes/ICMInlineVideo.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentModule.h" NS_ASSUME_NONNULL_BEGIN /* ICMInlineVideoFoundation does nothing. It exists for internal use. */ @interface ICMInlineVideoFoundation : ICLInlineContentModule @property (nonatomic, assign) BOOL videoAutoplayEnabled; // default = NO @property (nonatomic, assign) BOOL videoControlsEnabled; // default = YES @property (nonatomic, assign) BOOL videoLoopEnabled; // default = NO @property (nonatomic, assign) BOOL videoMuteEnabled; // default = NO @property (nonatomic, assign) NSTimeInterval videoStartTime; // default = 0 @property (nonatomic, assign) double videoPlaybackSpeed; // default = 1.0 | >= 0.125 and <= 6.0 + (NSTimeInterval)parseYouTubeEsqueTimestamp:(NSString *)timestamp; @end /* Proper class to sublcass if that is your thing. */ @interface ICMInlineVideo : ICMInlineVideoFoundation - (void)performAction; // checkVideo = YES - (void)performActionWithVideoCheck:(BOOL)checkVideo; - (void)performActionForAddress:(NSString *)address; // bypassVideoCheck = NO - (void)performActionForAddress:(NSString *)address bypassVideoCheck:(BOOL)bypassVideoCheck; - (void)performActionForURL:(NSURL *)url; // bypassVideoCheck = NO - (void)performActionForURL:(NSURL *)url bypassVideoCheck:(BOOL)bypassVideoCheck; + (ICLInlineContentModuleActionBlock)actionBlockForAddress:(NSString *)address; // bypassVideoCheck = NO + (ICLInlineContentModuleActionBlock)actionBlockForAddress:(NSString *)address bypassVideoCheck:(BOOL)bypassVideoCheck; + (ICLInlineContentModuleActionBlock)actionBlockForForURL:(NSURL *)url; // bypassVideoCheck = NO + (ICLInlineContentModuleActionBlock)actionBlockForForURL:(NSURL *)url bypassVideoCheck:(BOOL)bypassVideoCheck; - (void)notifyUnsafeToLoadVideo; @end /* Subclass that can be used for videos that should be treated as such: videoAutoplayEnabled = YES, videoControlsEnabled = NO, videoLoopEnabled = YES, videoMuteEnabled = YES */ @interface ICMInlineGifVideo : ICMInlineVideo @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Modules/Root Classes/ICMInlineVideo.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferences.h" #import "ICLHelpers.h" #import "ICLInlineContentModulePrivate.h" #import "ICLMediaAssessor.h" #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMInlineVideo () @property (nonatomic, strong, nullable) ICLMediaAssessor *videoCheck; @end @implementation ICMInlineVideo - (void)performAction { [self performActionWithVideoCheck:YES]; } - (void)performActionWithVideoCheck:(BOOL)checkVideo { if (checkVideo) { [self _performVideoCheck]; } else { [self _safeToLoadVideo]; } } - (void)performActionForURL:(NSURL *)url { [self performActionForURL:url bypassVideoCheck:NO]; } - (void)performActionForURL:(NSURL *)url bypassVideoCheck:(BOOL)bypassVideoCheck { NSParameterAssert(url != nil); NSAssert((self.videoCheck == nil), @"Module already initialized"); self.payload.urlToInline = url; [self performActionWithVideoCheck:(bypassVideoCheck == NO)]; } - (void)performActionForAddress:(NSString *)address { [self performActionForAddress:address bypassVideoCheck:NO]; } - (void)performActionForAddress:(NSString *)address bypassVideoCheck:(BOOL)bypassVideoCheck { NSParameterAssert(address != nil); NSURL *url = [ICLHelpers URLWithString:address]; [self performActionForURL:url bypassVideoCheck:bypassVideoCheck]; } - (void)_performVideoCheck { ICLPayload *payload = self.payload; ICLMediaAssessor *videoCheck = [ICLMediaAssessor assessorForURL:payload.urlToInline withType:ICLMediaTypeVideo completionBlock:^(ICLMediaAssessment *assessment, NSError *error) { BOOL safeToLoad = (error == nil); if (safeToLoad) { [self _safeToLoadVideo]; } else { [self _unsafeToLoadVideo]; [ICLMediaAssessor logError:error]; } self.videoCheck = nil; }]; self.videoCheck = videoCheck; [videoCheck resume]; } - (void)_unsafeToLoadVideo { [self notifyUnsafeToLoadVideo]; } - (void)_safeToLoadVideo { ICLPayloadMutable *payload = self.payload; double playbackSpeed = self.videoPlaybackSpeed; if (playbackSpeed < 0.125 || playbackSpeed > 6.0) { playbackSpeed = 1.0; } NSDictionary *templateAttributes = @{ @"anchorLink" : payload.address, @"classAttribute" : payload.classAttribute, @"preferredMaximumWidth" : @([TPCPreferences inlineMediaMaxWidth]), @"uniqueIdentifier" : payload.uniqueIdentifier, @"videoAutoplayEnabled" : @(self.videoAutoplayEnabled), @"videoControlsEnabled" : @(self.videoControlsEnabled), @"videoLoopEnabled" : @(self.videoLoopEnabled), @"videoMuteEnabled" : @(self.videoMuteEnabled), @"videoPlaybackSpeed" : @(playbackSpeed), @"videoStartTime" : @(self.videoStartTime), @"videoURL" : payload.addressToInline }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } - (void)notifyUnsafeToLoadVideo { [self cancel]; } #pragma mark - #pragma mark Action Block + (ICLInlineContentModuleActionBlock)actionBlockForForURL:(NSURL *)url { return [self actionBlockForForURL:url bypassVideoCheck:NO]; } + (ICLInlineContentModuleActionBlock)actionBlockForForURL:(NSURL *)url bypassVideoCheck:(BOOL)bypassVideoCheck { NSParameterAssert(url != nil); return [self actionBlockForAddress:url.absoluteString bypassVideoCheck:bypassVideoCheck]; } + (ICLInlineContentModuleActionBlock)actionBlockForAddress:(NSString *)address { return [self actionBlockForAddress:address bypassVideoCheck:NO]; } + (ICLInlineContentModuleActionBlock)actionBlockForAddress:(NSString *)address bypassVideoCheck:(BOOL)bypassVideoCheck { NSParameterAssert(address != nil); return [^(ICLInlineContentModule *module) { __weak ICMInlineVideo *moduleTyped = (id)module; [moduleTyped performActionForAddress:address bypassVideoCheck:bypassVideoCheck]; } copy]; } @end #pragma mark - #pragma mark Foundation @implementation ICMInlineVideoFoundation - (instancetype)initWithPayload:(ICLPayloadMutable *)payload inProcess:(ICLProcessMain *)process { if ((self = [super initWithPayload:payload inProcess:process])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { self.videoControlsEnabled = YES; self.videoPlaybackSpeed = 1.0; } + (BOOL)contentImageOrVideo { return YES; } - (nullable NSURL *)templateURL { return [RZMainBundle() URLForResource:@"ICMInlineVideo" withExtension:@"mustache" subdirectory:@"Components"]; } - (nullable NSArray *)styleResources { static NSArray *styleResources = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ styleResources = @[ [RZMainBundle() URLForResource:@"ICMInlineVideo" withExtension:@"css" subdirectory:@"Components"] ]; }); return styleResources; } - (nullable NSArray *)scriptResources { static NSArray *scriptResources = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ scriptResources = @[ [RZMainBundle() URLForResource:@"ICMInlineVideo" withExtension:@"js" subdirectory:@"Components"] ]; }); return scriptResources; } - (nullable NSString *)entrypoint { return @"_ICMInlineVideo"; } + (NSTimeInterval)parseYouTubeEsqueTimestamp:(NSString *)timestamp { NSParameterAssert(timestamp != nil); if (timestamp.isPositiveWholeNumber) { return timestamp.doubleValue; } __block NSTimeInterval startTime = 0; __block BOOL matchedHour = NO; __block BOOL matchedMinute = NO; __block BOOL matchedSecond = NO; [timestamp enumerateMatchesOfRegularExpression:@"[0-9]+[hms]" withBlock:^(NSRange range, BOOL *stop) { NSString *fragment = [timestamp substringWithRange:range]; NSString *fragmentUnit = [fragment substringAtIndex:0 toLength:(-1)]; NSString *fragmentValue = [fragment substringAtIndex:(-1) toLength:0]; /* Could use dictionary to index each formatter, but that seemed like overkill for this implemention. */ if (matchedHour == NO && [fragmentUnit isEqualToString:@"h"]) { matchedHour = YES; startTime += (fragmentValue.integerValue * 3600); // 1 hour } else if (matchedMinute == NO && [fragmentUnit isEqualToString:@"m"]) { matchedMinute = YES; startTime += (fragmentValue.integerValue * 60); // 1 minute } else if (matchedSecond == NO && [fragmentUnit isEqualToString:@"s"]) { matchedSecond = YES; startTime += fragmentValue.integerValue; } if (matchedHour && matchedMinute && matchedSecond) { *stop = YES; } } options:NSCaseInsensitiveSearch]; return startTime; } @end #pragma mark - #pragma mark Gif Video @implementation ICMInlineGifVideo - (instancetype)initWithPayload:(ICLPayloadMutable *)payload inProcess:(ICLProcessMain *)process { if ((self = [super initWithPayload:payload inProcess:process])) { [self prepareInitialState]; return self; } return nil; } - (void)prepareInitialState { [super prepareInitialState]; self.videoAutoplayEnabled = YES; self.videoControlsEnabled = NO; self.videoLoopEnabled = YES; self.videoMuteEnabled = YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLHelpers.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLHelpers.h" NS_ASSUME_NONNULL_BEGIN @implementation ICLHelpers + (nullable NSURL *)URLWithString:(NSString *)address { NSParameterAssert(address != nil); if ([address hasPrefix:@"//"]) { address = [@"https:" stringByAppendingString:address]; } return [NSURL URLWithString:address]; } @end #pragma mark - #pragma mark Errors @implementation ICLHelpers (Errors) + (NSError *)genericValidationFailedError { static NSError *error = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ error = [NSError errorWithDomain:ICLInlineContentErrorDomain code:1003 userInfo:@{ NSLocalizedDescriptionKey : @"Validation failed" }]; }); return error; } @end #pragma mark - #pragma mark JSON @implementation ICLHelpers (JSON) + (NSURLSessionDataTask *)requestJSONObject:(NSString *)objectKey ofType:(Class)objectType inHierarchy:(nullable NSArray *)hierarchy fromURL:(NSURL *)url completionBlock:(void (^)(id _Nullable object))completionBlock { NSParameterAssert(objectKey != nil); NSParameterAssert(objectType != NULL); NSParameterAssert(url != nil); NSParameterAssert(completionBlock != nil); return [self requestJSONDataFromURL:url completionBlock:^(BOOL success, NSDictionary * _Nullable data) { /* Return nothing if underlying request failed. */ if (success == NO) { completionBlock(nil); return; } /* Traverse hiearchy */ /* hierarchy is the path we will traverse to find objectKey. All keys assigned to hierarchy are expected to be a dictionary. If a key in hierarchy does not exist or is not a dictionary, then the request exits. */ NSDictionary *currentContext = data; if (hierarchy) { for (NSString *hierarchyKey in hierarchy) { id hierarchyContext = [currentContext dictionaryForKey:hierarchyKey]; /* Return nothing if we cannot go deeper. */ if (hierarchyContext == nil) { completionBlock(nil); return; } currentContext = hierarchyContext; } } /* Get object value and check type */ id objectValue = currentContext[objectKey]; /* Object is not a type we are interested in */ if ([objectValue isKindOfClass:objectType] == NO) { completionBlock(nil); return; } /* Object is a type we are interested in */ completionBlock(objectValue); }]; } + (NSURLSessionDataTask *)requestJSONObject:(NSString *)objectKey ofType:(Class)objectType inHierarchy:(nullable NSArray *)hierarchy fromAddress:(NSString *)address completionBlock:(void (^)(id _Nullable object))completionBlock { NSParameterAssert(objectKey != nil); NSParameterAssert(objectType != NULL); NSParameterAssert(address != nil); NSParameterAssert(completionBlock != nil); NSURL *url = [NSURL URLWithString:address]; return [self requestJSONObject:objectKey ofType:objectType inHierarchy:hierarchy fromURL:url completionBlock:completionBlock]; } + (NSURLSessionDataTask *)requestJSONDataFromURL:(NSURL *)url completionBlock:(void (^)(BOOL success, NSDictionary * _Nullable data))completionBlock { NSParameterAssert(url != nil); NSParameterAssert(completionBlock != nil); NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *sessionTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { /* Report error if data is nil or we have a non-OK response from server. */ if (data == nil || ((NSHTTPURLResponse *)response).statusCode != 200) { if (error) { LogToConsoleError("Request failed with error: %{public}@", error.localizedDescription); } completionBlock(NO, nil); return; } /* Decode JSON data */ NSError *decodedJsonError; NSDictionary *decodedJson = [NSJSONSerialization JSONObjectWithData:data options:0 error:&decodedJsonError]; if (decodedJson == nil) { LogToConsoleError("Failed to decode response: %{public}@", decodedJsonError.localizedDescription); completionBlock(NO, nil); return; } if ([decodedJson isKindOfClass:[NSDictionary class]] == NO) { completionBlock(NO, nil); return; } /* Post JSON data */ completionBlock(YES, decodedJson); }]; [sessionTask resume]; return sessionTask; } + (NSURLSessionDataTask *)requestJSONDataFromAddress:(NSString *)address completionBlock:(void (^)(BOOL success, NSDictionary * _Nullable data))completionBlock { NSParameterAssert(address != nil); NSParameterAssert(completionBlock != nil); NSURL *url = [NSURL URLWithString:address]; return [self requestJSONDataFromURL:url completionBlock:completionBlock]; } @end #pragma mark - #pragma mark Strings @implementation NSString (ICLHelpers) - (BOOL)isDomain:(NSString *)domain { NSParameterAssert(domain != nil); return [self isEqualToString:domain]; } - (BOOL)isDomainOrSubdomain:(NSString *)domain { NSParameterAssert(domain != nil); return ([self isEqualToString:domain] || [self hasSuffix:[@"." stringByAppendingString:domain]]); } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLInlineContentModule.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "ICLPayloadPrivate.h" #import "ICLProcessMainPrivate.h" #import "ICLInlineContentModuleInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation ICLInlineContentModule - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithPayload:(ICLPayloadMutable *)payload inProcess:(ICLProcessMain *)process { NSParameterAssert(payload != nil); NSParameterAssert(process != nil); if ((self = [super init])) { self->_payload = payload; self->_process = process; [self mergePropertiesIntoPayload]; return self; } return nil; } - (instancetype)initWithDeferredModule:(ICLInlineContentModule *)module { NSParameterAssert(module != nil); if ((self = [super init])) { self->_payload = [[ICLPayloadMutable alloc] initWithDeferredPayload:module.payload]; self->_process = module->_process; [self mergePropertiesIntoPayload]; return self; } return nil; } - (void)mergePropertiesIntoPayload { NSArray *scriptResources = self.scriptResources; if (scriptResources) { self.payload.scriptResources = scriptResources; } NSArray *styleResources = self.styleResources; if (styleResources) { self.payload.styleResources = styleResources; } NSString *entrypoint = self.entrypoint; if (entrypoint) { self.payload.entrypoint = entrypoint; } } - (nullable NSURL *)templateURL { return nil; } - (nullable GRMustacheTemplate *)template { NSURL *templateURL = self.templateURL; if (templateURL == nil || templateURL.isFileURL == NO) { return nil; } NSError *templateLoadError; GRMustacheTemplate *template = [GRMustacheTemplate templateFromContentsOfURL:templateURL error:&templateLoadError]; if (template == nil) { LogToConsoleError("Failed to load template '%{public}@': %{public}@", templateURL.standardizedTildePath, templateLoadError.localizedDescription); } return template; } + (nullable NSArray *)domains { return nil; } + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { return nil; } + (nullable SEL)actionForURL:(NSURL *)url { return NULL; } - (nullable NSArray *)styleResources { return nil; } - (nullable NSArray *)scriptResources { return nil; } - (nullable NSString *)entrypoint { return nil; } + (BOOL)contentImageOrVideo { return NO; } + (BOOL)contentUntrusted { return NO; } + (BOOL)contentNotSafeForWork { return NO; } + (BOOL)contentIsFile { return NO; } @end #pragma mark - #pragma mark Completion @implementation ICLInlineContentModule (Completion) - (void)_finalizeAll { self->_moduleFinalized = YES; self->_process = nil; } - (void)finalize { [self finalizeWithError:nil]; } - (void)finalizeWithError:(nullable NSError *)error { NSAssert((self->_moduleFinalized == NO), @"Module already finalized"); [self finalizePreflight]; [self->_process _finalizeModule:self withError:error]; [self _finalizeAll]; } - (void)cancel { NSAssert((self->_moduleFinalized == NO), @"Module already cancelled"); [self finalizePreflight]; [self->_process _cancelModule:self]; [self _finalizeAll]; } + (BOOL)isTypeDeferrable:(ICLMediaType)type { return (type == ICLMediaTypeImage || type == ICLMediaTypeVideo || type == ICLMediaTypeVideoGif); } - (void)deferAsType:(ICLMediaType)type { [self deferAsType:type performCheck:YES]; } - (void)deferAsType:(ICLMediaType)type performCheck:(BOOL)performCheck { NSAssert((self->_moduleFinalized == NO), @"Module already deferred"); [self finalizePreflight]; [self->_process _deferModule:self asType:type performCheck:performCheck]; [self _finalizeAll]; } @end #pragma mark - #pragma mark Completion (Private) @implementation ICLInlineContentModule (CompletionPrivate) - (void)finalizePreflight { } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLMediaAssessment.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "ICLMediaAssessment.h" #import "ICLMediaAssessmentInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation ICLMediaAssessment - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithURL:(NSURL *)url asType:(ICLMediaType)type { NSParameterAssert(url != nil); if ((self = [super init])) { self->_url = [url copy]; self->_type = type; [self populateDefaultsPostflight]; return self; } return nil; } - (BOOL)populateWithDecoder:(NSCoder *)aDecoder { self->_url = [aDecoder decodeObjectOfClass:[NSURL class] forKey:@"url"]; self->_type = [aDecoder decodeUnsignedIntegerForKey:@"type"]; self->_contentType = [aDecoder decodeStringForKey:@"contentType"]; self->_contentLength = [aDecoder decodeUnsignedIntegerForKey:@"contentLength"]; return YES; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self->_url forKey:@"url"]; [aCoder encodeUnsignedInteger:self->_type forKey:@"type"]; [aCoder encodeString:self->_contentType forKey:@"contentType"]; [aCoder encodeUnsignedInteger:self->_contentLength forKey:@"contentLength"]; } + (BOOL)supportsSecureCoding { return YES; } - (void)populateDefaultsPostflight { SetVariableIfNil(self->_contentType, @"application/binary"); } - (void)initializedClassHealthCheck { NSParameterAssert(self->_url != nil); NSParameterAssert(self->_contentType != nil); } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { ICLMediaAssessment *object = [self allocForCopyAsMutable:mutableCopy]; object->_url = self->_url; object->_type = self->_type; object->_contentType = self->_contentType; object->_contentLength = self->_contentLength; return [object initOnCopy]; } - (__kindof XRPortablePropertyObject *)mutableClass { return [ICLMediaAssessmentMutable self]; } @end #pragma mark - @implementation ICLMediaAssessmentMutable @dynamic type; @dynamic contentType; @dynamic contentLength; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [ICLMediaAssessment self]; } - (void)setType:(ICLMediaType)type { if (self->_type != type) { self->_type = type; } } - (void)setContentType:(NSString *)contentType { if (self->_contentType != contentType) { self->_contentType = [contentType copy]; } } - (void)setContentLength:(unsigned long long)contentLength { if (self->_contentLength != contentLength) { self->_contentLength = contentLength; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLMediaAssessor.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "TPCPreferences.h" #import "ICLMediaAssessment.h" #import "ICLMediaAssessor.h" NS_ASSUME_NONNULL_BEGIN /* Hardcoded maximum width for images (and maybe other media) */ #define _assessorMaximumImageWidth 7200 NSString * const ICLMediaAssessorErrorDomain = @"ICLMediaAssessorErrorDomain"; @interface ICLMediaAssessorConfiguration : NSObject @property (nonatomic, copy) ICLMediaAssessorCompletionBlock completionBlock; @property (nonatomic, assign) ICLMediaType expectedType; @property (nonatomic, copy) NSURL *url; @end @interface ICLMediaAssessorLimits : NSObject @property (nonatomic, assign) NSUInteger imageMaximumWidth; @property (nonatomic, assign) NSUInteger imageMaximumHeight; @property (nonatomic, assign) unsigned long long imageMaximumFilesize; @end @interface ICLMediaAssessorRequest : NSObject @property (nonatomic, strong, nullable) NSURLSession *session; @property (nonatomic, strong, nullable) NSURLSessionTask *task; @property (nonatomic, copy, nullable) NSError *alternateError; @property (nonatomic, assign) BOOL doNotFinalize; @end @interface ICLMediaAssessorState : NSObject @property (nonatomic, copy, nullable) ICLMediaAssessment *assessment; @property (nonatomic, assign) BOOL performExtendedValidation; @end @interface ICLMediaAssessor () @property (nonatomic, strong, nullable) ICLMediaAssessorConfiguration *config; @property (nonatomic, strong, nullable) ICLMediaAssessorLimits *limits; @property (nonatomic, strong, nullable) ICLMediaAssessorRequest *request; @property (nonatomic, strong, nullable) ICLMediaAssessorState *state; @end @implementation ICLMediaAssessor #pragma mark - #pragma mark Construction - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } + (instancetype)assessorForAddress:(NSString *)address completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock { return [self assessorForAddress:address withType:ICLMediaTypeUnknown completionBlock:completionBlock]; } + (instancetype)assessorForURL:(NSURL *)url completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock { return [self assessorForURL:url withType:ICLMediaTypeUnknown completionBlock:completionBlock]; } + (instancetype)assessorForAddress:(NSString *)address withType:(ICLMediaType)type completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock { ICLMediaAssessor *object = [[self alloc] initWithAddress:address withType:type completionBlock:completionBlock]; return object; } + (instancetype)assessorForURL:(NSURL *)url withType:(ICLMediaType)type completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock { ICLMediaAssessor *object = [[self alloc] initWithURL:url withType:type completionBlock:completionBlock]; return object; } - (instancetype)initWithAddress:(NSString *)address withType:(ICLMediaType)type completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock { NSParameterAssert(address != nil); NSParameterAssert(completionBlock != nil); NSURL *url = [NSURL URLWithString:address]; return [self initWithURL:url withType:type completionBlock:completionBlock]; } - (instancetype)initWithURL:(NSURL *)url withType:(ICLMediaType)type completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock { NSParameterAssert(url != nil); NSParameterAssert(url.isFileURL == NO); NSParameterAssert(completionBlock != nil); if ((self = [super init])) { [self prepareToAssessURL:url withType:type completionBlock:completionBlock]; return self; } return nil; } - (void)prepareToAssessURL:(NSURL *)url withType:(ICLMediaType)type completionBlock:(ICLMediaAssessorCompletionBlock)completionBlock { NSParameterAssert(url != nil); NSParameterAssert(completionBlock != nil); /* Prepare configuration */ ICLMediaAssessorConfiguration *config = [ICLMediaAssessorConfiguration new]; config.completionBlock = completionBlock; config.expectedType = type; config.url = url; self.config = config; } #pragma mark - #pragma mark Actions (Public) - (void)resume { [self _assess]; } - (void)suspend { [self _cancel]; } #pragma mark - #pragma mark Actions (Private) - (void)_assess { NSAssert((self.request == nil), @"An assessment is already in progress"); ICLMediaAssessorConfiguration *config = self.config; NSAssert((config != nil), @"-assess called after an assessment finalized"); /* Prepare request */ ICLMediaAssessorRequest *request = [ICLMediaAssessorRequest new]; NSURLSession *urlSession = [NSURLSession sessionWithConfiguration:[self.class _sharedSessionConfiguration] delegate:(id)self delegateQueue:nil]; request.session = urlSession; /* We use a data task which is always GET. Many services block HEAD requests, so we use GET. When we are only interested in the headers, we close the connection after receiving them, so we don't waste resources. */ NSURLSessionDataTask *urlSessionTask = [urlSession dataTaskWithURL:config.url]; request.task = urlSessionTask; self.request = request; /* Prepare limits */ ICLMediaType expectedType = self.config.expectedType; if (expectedType == ICLMediaTypeUnknown || expectedType == ICLMediaTypeImage) { ICLMediaAssessorLimits *limits = [ICLMediaAssessorLimits new]; limits.imageMaximumWidth = _assessorMaximumImageWidth; limits.imageMaximumHeight = [TPCPreferences inlineMediaMaxHeight]; limits.imageMaximumFilesize = [TPCPreferences inlineImagesMaxFilesize]; self.limits = limits; } /* Perform request */ [urlSessionTask resume]; } - (void)_cancel { [self _cancelRequest]; } #pragma mark - #pragma mark Utilities - (void)_cancelRequest { ICLMediaAssessorRequest *request = self.request; if (request == nil) { return; } request.doNotFinalize = YES; NSURLSession *session = request.session; [session invalidateAndCancel]; } - (void)_flushRequestState { self.limits = nil; self.state = nil; self.request = nil; } - (void)_finalizeAssessmentWithError:(nullable NSError *)error { if (error.isURLSessionCancelError) { error = self.request.alternateError; } [self _performCompletionBlockWithError:error]; [self _flushRequestState]; self.config = nil; } - (void)_performCompletionBlockWithError:(nullable NSError *)error { ICLMediaAssessorConfiguration *config = self.config; ICLMediaAssessment *assessment = self.state.assessment; /* This condition is typically true when we refuse an authentication challenge. */ if (error == nil && assessment == nil) { error = [self _errorWithDescription:@"Assessment failed" code:ICLMediaAssessorErrorCodeAssessmentFailed]; } config.completionBlock(assessment, error); } - (NSError *)_errorWithDescription:(NSString *)errorDescription code:(ICLMediaAssessorErrorCode)errorCode { NSParameterAssert(errorDescription != nil); return [NSError errorWithDomain:ICLMediaAssessorErrorDomain code:errorCode userInfo:@{ NSLocalizedDescriptionKey : errorDescription }]; } #pragma mark - #pragma mark URL Session Delegate + (NSURLSessionConfiguration *)_sharedSessionConfiguration { static NSURLSessionConfiguration *config = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; /* Ignore local caches */ config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; /* Do not send cookies from local store */ config.HTTPShouldSetCookies = NO; /* Do not allow cookies to be set */ config.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever; }); return config; } - (nullable ICLMediaAssessorState *)_readHeadersInWithError:(NSError **)error { NSParameterAssert(error != NULL); NSHTTPURLResponse *response = (id)self.request.task.response; /* Read in status code */ if (response.statusCode != 200) { *error = [self _errorWithDescription:@"Endpoint did not respond with OK (200)" code:ICLMediaAssessorErrorCodeUnexpectedStatusCode]; return nil; } /* Read in content type */ NSString *contentType = response.MIMEType; if (contentType.length > 128) { *error = [self _errorWithDescription:@"Content-Type header is improperly formatted" code:ICLMediaAssessorErrorCodeMalformedContentType]; return nil; } /* Read in content length */ long long contentLength = response.expectedContentLength; if (contentLength <= 0) { *error = [self _errorWithDescription:@"Content-Length header is improperly formatted" code:ICLMediaAssessorErrorCodeMalformedContentLength]; return nil; } /* Figure out what type of media this is */ ICLMediaType mediaType = ICLMediaTypeOther; BOOL performExtendedValidation = NO; /* Content is an image */ if ([[self.class validImageContentTypes] containsObject:contentType]) { mediaType = ICLMediaTypeImage; } /* Content is a video */ else if ([[self.class validVideoContentTypes] containsObject:contentType]) { mediaType = ICLMediaTypeVideo; } /* Is this a type we are interested in? */ ICLMediaType expectedType = self.config.expectedType; if (expectedType != ICLMediaTypeUnknown && expectedType != mediaType) { *error = [self _errorWithDescription:@"Unexpected media type" code:ICLMediaAssessorErrorCodeUnexpectedType]; return nil; } /* Perform basic validation */ switch (mediaType) { case ICLMediaTypeImage: { ICLMediaAssessorLimits *limits = self.limits; /* Limit maximum filesize */ if (contentLength > limits.imageMaximumFilesize) { *error = [self _errorWithDescription:@"Content-Length exceeds maximum allowed" code:ICLMediaAssessorErrorCodeContentLengthExceeded]; return nil; } /* Limiting the height of an image requires us to download the contents of the image first, which we do by setting the performExtendedValidation flag on the state. */ if (limits.imageMaximumHeight > 0) { performExtendedValidation = YES; } break; } default: { break; } } /* Complete read in */ ICLMediaAssessmentMutable *assessment = [[ICLMediaAssessmentMutable alloc] initWithURL:response.URL asType:mediaType]; assessment.contentType = contentType; assessment.contentLength = contentLength; ICLMediaAssessorState *state = [ICLMediaAssessorState new]; state.assessment = assessment; state.performExtendedValidation = performExtendedValidation; return state; } #pragma mark #pragma mark URL Session Delegate Cont. - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { /* Response might not be an HTTP response if we end up receiving a redirect to a data URL. */ if ([response isKindOfClass:[NSHTTPURLResponse class]] == NO) { self.request.alternateError = [self _errorWithDescription:@"Invalid response type (not HTTP)" code:ICLMediaAssessorErrorCodeUnexpectedResponse]; completionHandler(NSURLSessionResponseCancel); return; } /* Perform basic assessment of request. -readHeadersIn returning a nil value indicates that something was erroneous and we should not continue. */ NSError *readHeadersInError; ICLMediaAssessorState *state = [self _readHeadersInWithError:&readHeadersInError]; if (state == nil) { self.request.alternateError = readHeadersInError; completionHandler(NSURLSessionResponseCancel); return; } self.state = state; /* Some requests may require us to download the data of the media to perform extended validation, such as when the user limits the height of images. */ /* When we don't have to perform extended validation, we can just cancel any further actions because we already have enough from the headers. */ if (state.performExtendedValidation == NO) { completionHandler(NSURLSessionResponseCancel); return; } /* Change to download task */ completionHandler(NSURLSessionResponseBecomeDownload); } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler { /* Do not perform caching */ completionHandler(nil); } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler { /* Follow redirects */ completionHandler(request); } - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler { /* Refuse challenge requests */ NSString *authenticationMethod = challenge.protectionSpace.authenticationMethod; if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic] || [authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPDigest]) { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); return; } completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask { self.request.task = downloadTask; } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { /* According to the documentation for NSURLSession, the file at the URL will be deleted once this delegate call completes which means we do not have to do much: Just perform our extended validation, then return. */ NSError *extendedValidationError; if ([self _performExtendedValidationAtURL:location withError:&extendedValidationError]) { return; /* Success */ } self.request.alternateError = extendedValidationError; [session invalidateAndCancel]; } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { /* Don't allow our Content-Length to lie to us/ */ if ([self _downloadProgressExceededMaximumFilesize:totalBytesWritten] == NO) { return; /* Success */ } self.request.alternateError = [self _errorWithDescription:@"Maximum response size exceeded" code:ICLMediaAssessorErrorCodeContentLengthExceeded]; [session invalidateAndCancel]; } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error { /* If -suspend was called on this object, cleanup the request but do not finalize. */ if (self.request.doNotFinalize) { [self _flushRequestState]; return; } [self _finalizeAssessmentWithError:error]; } #pragma mark - #pragma mark Basic Validation + (NSArray *)validImageContentTypes { static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = @[@"image/gif", @"image/jpeg", @"image/png", @"image/svg+xml", @"image/tiff", @"image/x-ms-bmp"]; }); return cachedValue; } + (NSArray *)validVideoContentTypes { static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = @[@"video/3gpp", @"video/3gpp2", @"video/mp4", @"video/quicktime", @"video/x-m4v"]; }); return cachedValue; } #pragma mark - #pragma mark Extended Validation - (BOOL)_downloadProgressExceededMaximumFilesize:(unsigned long long)downloadProgress { unsigned long long maximumFilesize = 0; switch (self.state.assessment.type) { case ICLMediaTypeImage: { maximumFilesize = self.limits.imageMaximumFilesize; break; } default: { break; } } if (maximumFilesize == 0) { return NO; /* Success */ } return (downloadProgress > maximumFilesize); } - (BOOL)_performExtendedValidationAtURL:(NSURL *)url withError:(NSError **)error { NSParameterAssert(url != nil); NSParameterAssert(error != NULL); switch (self.state.assessment.type) { case ICLMediaTypeImage: { return [self _performExtendedValidationForImageAtURL:url withError:error]; break; } default: { break; } } return YES; /* Success */ } - (BOOL)_performExtendedValidationForImageAtURL:(NSURL *)url withError:(NSError **)error { NSParameterAssert(url != nil); NSParameterAssert(error != NULL); CGImageSourceRef image = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL); if (image == NULL) { *error = [self _errorWithDescription:@"Image validation: CGImageSourceCreateWithURL() returned NULL" code:ICLMediaAssessorErrorCodeAssessmentFailed]; return NO; } CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(image, 0, NULL); if (imageProperties == NULL) { CFRelease(image); *error = [self _errorWithDescription:@"Image validation: CGImageSourceCopyPropertiesAtIndex() returned NULL" code:ICLMediaAssessorErrorCodeAssessmentFailed]; return NO; } NSNumber *imageWidth = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth); NSNumber *imageHeight = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight); CFRelease(image); CFRelease(imageProperties); ICLMediaAssessorLimits *limits = self.limits; if (imageWidth.integerValue > limits.imageMaximumWidth) { *error = [self _errorWithDescription:@"Image validation: Maximum width exceeded" code:ICLMediaAssessorErrorCodeMaximumWidthExceeded]; return NO; } else if (imageHeight.integerValue > limits.imageMaximumHeight) { *error = [self _errorWithDescription:@"Image validation: Maximum height exceeded" code:ICLMediaAssessorErrorCodeMaximumHeightExceeded]; return NO; } return YES; /* Success */ } #pragma mark - #pragma mark Logging + (void)logError:(NSError *)error { NSParameterAssert(error != nil); if ([error.domain isEqualToString:ICLMediaAssessorErrorDomain] == NO) { return; } ICLMediaAssessorErrorCode errorCode = error.code; switch (errorCode) { case ICLMediaAssessorErrorCodeAssessmentFailed: case ICLMediaAssessorErrorCodeUnexpectedStatusCode: case ICLMediaAssessorErrorCodeMalformedContentType: case ICLMediaAssessorErrorCodeMalformedContentLength: case ICLMediaAssessorErrorCodeUnexpectedResponse: { LogToConsoleDebug("Assessor fatal error: %{public}@", error.localizedDescription); } case ICLMediaAssessorErrorCodeUnexpectedType: case ICLMediaAssessorErrorCodeContentLengthExceeded: case ICLMediaAssessorErrorCodeMaximumWidthExceeded: case ICLMediaAssessorErrorCodeMaximumHeightExceeded: { LogToConsoleDebug("Assessor validation error: %{public}@", error.localizedDescription); } } // switch() } @end #pragma mark - @implementation ICLMediaAssessorConfiguration @end @implementation ICLMediaAssessorLimits @end @implementation ICLMediaAssessorRequest @end @implementation ICLMediaAssessorState @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLPayloadLocal.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "ICLPayloadInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation ICLPayload (ICLPayloadPrivate) - (nullable instancetype)initWithURL:(NSURL *)url withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index inView:(NSString *)viewIdentifier { NSParameterAssert(url != nil); NSParameterAssert(uniqueIdentifier != nil); NSParameterAssert(lineNumber != nil); NSParameterAssert(viewIdentifier != nil); if ((self = [super init])) { self->_url = [url copy]; self->_lineNumber = [lineNumber copy]; self->_index = index; self->_uniqueIdentifier = [uniqueIdentifier copy]; self->_viewIdentifier = [viewIdentifier copy]; [self populateDefaultsPostflight]; return self; } return nil; } - (instancetype)initWithDeferredPayload:(ICLPayload *)payload { NSParameterAssert(payload != nil); if ((self = [super init])) { /* All values are immutable which means we don't need to copy their contents. */ self->_url = payload.url; self->_urlToInline = payload.urlToInline; self->_lineNumber = payload.lineNumber; self->_index = payload.index; self->_uniqueIdentifier = payload.uniqueIdentifier; self->_viewIdentifier = payload.viewIdentifier; self->_classAttribute = payload.classAttribute; [self populateDefaultsPostflight]; return self; } return nil; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLPayloadShared.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "NSObjectHelperPrivate.h" #import "ICLPayload.h" #import "ICLPayloadInternal.h" NS_ASSUME_NONNULL_BEGIN @implementation ICLPayload - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initOnMutate { if ((self = [super initOnMutate])) { [self populateDefaultsPostflight]; return self; } return nil; } - (BOOL)populateWithDecoder:(NSCoder *)aDecoder { self->_contentLength = [aDecoder decodeUnsignedIntegerForKey:@"contentLength"]; self->_contentSize = [aDecoder decodeSizeForKey:@"contentSize"]; self->_styleResources = [aDecoder decodeObjectOfClasses:[NSSet setWithObjects:[NSArray class], [NSURL class], nil] forKey:@"styleResources"]; self->_scriptResources = [aDecoder decodeObjectOfClasses:[NSSet setWithObjects:[NSArray class], [NSURL class], nil] forKey:@"scriptResources"]; self->_html = [aDecoder decodeStringForKey:@"html"]; self->_entrypoint = [aDecoder decodeStringForKey:@"entrypoint"]; self->_entrypointPayload = [aDecoder decodeDictionaryForKey:@"entrypointPayload"]; self->_url = [aDecoder decodeObjectOfClass:[NSURL class] forKey:@"url"]; self->_urlToInline = [aDecoder decodeObjectOfClass:[NSURL class] forKey:@"urlToInline"]; self->_lineNumber = [aDecoder decodeStringForKey:@"lineNumber"]; self->_uniqueIdentifier = [aDecoder decodeStringForKey:@"uniqueIdentifier"]; self->_viewIdentifier = [aDecoder decodeStringForKey:@"viewIdentifier"]; self->_index = [aDecoder decodeUnsignedIntegerForKey:@"index"]; self->_classAttribute = [aDecoder decodeStringForKey:@"classAttribute"]; return YES; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeUnsignedInteger:self->_contentLength forKey:@"contentLength"]; [aCoder encodeSize:self->_contentSize forKey:@"contentSize"]; [aCoder maybeEncodeObject:self->_styleResources forKey:@"styleResources"]; [aCoder maybeEncodeObject:self->_scriptResources forKey:@"scriptResources"]; [aCoder encodeString:self->_html forKey:@"html"]; [aCoder maybeEncodeObject:self->_entrypoint forKey:@"entrypoint"]; [aCoder maybeEncodeObject:self->_entrypointPayload forKey:@"entrypointPayload"]; [aCoder encodeObject:self->_url forKey:@"url"]; [aCoder encodeObject:self->_urlToInline forKey:@"urlToInline"]; [aCoder encodeString:self->_lineNumber forKey:@"lineNumber"]; [aCoder encodeString:self->_uniqueIdentifier forKey:@"uniqueIdentifier"]; [aCoder encodeString:self->_viewIdentifier forKey:@"viewIdentifier"]; [aCoder encodeUnsignedInteger:self->_index forKey:@"index"]; [aCoder encodeString:self->_classAttribute forKey:@"classAttribute"]; } + (BOOL)supportsSecureCoding { return YES; } - (void)populateDefaultsPostflight { if (self.initializedAsCopy == NO) { self->_contentSize = NSZeroSize; } SetVariableIfNil(self->_urlToInline, self->_url); SetVariableIfNil(self->_styleResources, @[]); SetVariableIfNil(self->_scriptResources, @[]); SetVariableIfNil(self->_html, @""); SetVariableIfNil(self->_classAttribute, @""); } - (void)initializedClassHealthCheck { NSParameterAssert(self->_html != nil); NSParameterAssert(self->_url != nil); NSParameterAssert(self->_urlToInline != nil); NSParameterAssert(self->_lineNumber != nil); NSParameterAssert(self->_uniqueIdentifier != nil); NSParameterAssert(self->_viewIdentifier != nil); NSParameterAssert(self->_classAttribute != nil); } - (id)copyAsMutable:(BOOL)mutableCopy uniquing:(BOOL)uniquing { ICLPayload *object = [self allocForCopyAsMutable:mutableCopy]; object->_contentLength = self->_contentLength; object->_contentSize = self->_contentSize; object->_styleResources = self->_styleResources; object->_scriptResources = self->_scriptResources; object->_html = self->_html; object->_entrypoint = self->_entrypoint; object->_entrypointPayload = self->_entrypointPayload; object->_url = self->_url; object->_urlToInline = self->_urlToInline; object->_lineNumber = self->_lineNumber; object->_uniqueIdentifier = self->_uniqueIdentifier; object->_viewIdentifier = self->_viewIdentifier; object->_index = self->_index; object->_classAttribute = self->_classAttribute; return [object initOnCopy]; } - (__kindof XRPortablePropertyObject *)mutableClass { return [ICLPayloadMutable self]; } - (NSDictionary> *)entrypointPayload { NSDictionary *payload = self->_entrypointPayload; if (payload == nil) { return [self entrypointPayloadDefaultContext]; } return payload; } - (NSDictionary> *)entrypointPayloadDefaultContext { return @{ @"class" : self->_classAttribute, @"html" : self->_html, @"url" : self->_url, @"urlToInline" : self->_urlToInline, @"lineNumber" : self->_lineNumber, @"uniqueIdentifier" : self->_uniqueIdentifier }; } - (void)entrypointPayloadSetContext { /* Set context to payload that module sets. */ /* The values set in the context don't change so we are safe setting and forgetting. */ NSDictionary *payload = self->_entrypointPayload; if (payload == nil) { return; } NSDictionary *payloadToSet = [self entrypointPayloadDefaultContext]; self->_entrypointPayload = [payload dictionaryByAddingEntries:payloadToSet]; } - (NSString *)address { return self->_url.absoluteString; } - (NSString *)addressToInline { return self->_urlToInline.absoluteString; } @end #pragma mark - @implementation ICLPayloadMutable @dynamic urlToInline; @dynamic contentLength; @dynamic contentSize; @dynamic styleResources; @dynamic scriptResources; @dynamic html; @dynamic entrypoint; @dynamic entrypointPayload; @dynamic classAttribute; + (BOOL)isMutable { return YES; } - (__kindof XRPortablePropertyObject *)immutableClass { return [ICLPayload self]; } DESIGNATED_INITIALIZER_EXCEPTION_BODY_BEGIN - (instancetype)init { return [self initOnMutate]; } DESIGNATED_INITIALIZER_EXCEPTION_BODY_END - (void)setUrlToInline:(NSURL *)urlToInline { NSParameterAssert(urlToInline != nil); NSParameterAssert(urlToInline.isFileURL == NO); if (self->_urlToInline != urlToInline) { self->_urlToInline = [urlToInline copy]; } } - (void)setContentLength:(unsigned long long)contentLength { if (self->_contentLength != contentLength) { self->_contentLength = contentLength; } } - (void)setContentSize:(NSSize)contentSize { self->_contentSize = contentSize; } - (void)setStyleResources:(NSArray *)styleResources { NSParameterAssert(styleResources != nil); if (self->_styleResources != styleResources) { self->_styleResources = [styleResources copy]; } } - (void)setScriptResources:(NSArray *)scriptResources { NSParameterAssert(scriptResources != nil); if (self->_scriptResources != scriptResources) { self->_scriptResources = [scriptResources copy]; } } - (void)setHtml:(NSString *)html { NSParameterAssert(html != nil); if (self->_html != html) { self->_html = [html copy]; } } - (void)setEntrypoint:(nullable NSString *)entrypoint { if (self->_entrypoint != entrypoint) { self->_entrypoint = [entrypoint copy]; } } - (void)setEntrypointPayload:(nullable NSDictionary> *)entrypointPayload { if (self->_entrypointPayload != entrypointPayload) { self->_entrypointPayload = [entrypointPayload copy]; [self entrypointPayloadSetContext]; } } - (void)setClassAttribute:(NSString *)classAttribute { NSParameterAssert(classAttribute != nil); if (self->_classAttribute != classAttribute) { self->_classAttribute = [classAttribute copy]; } } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLPluginManager.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* TODO: Entitlements do not allow access to plugins outside app in Standard Release */ #import "ICLPluginProtocol.h" #import "ICLPluginManagerPrivate.h" NS_ASSUME_NONNULL_BEGIN @interface ICLPluginManager () @property (nonatomic, assign) BOOL pluginsLoaded; @property (nonatomic, copy) NSArray *loadedPlugins; @property (nonatomic, copy, nullable) NSArray *loadedModules; @end @implementation ICLPluginManager + (ICLPluginManager *)sharedPluginManager { static ICLPluginManager *sharedSelf = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedSelf = [[self alloc] init]; }); return sharedSelf; } - (NSURL *)_bundledPluginsURL { return [RZMainBundle() URLForResource:@"Extensions" withExtension:nil]; } - (void)loadPluginsAtLocations:(NSArray *)pluginLocations { NSParameterAssert(pluginLocations != nil); NSAssert((self.pluginsLoaded == NO), @"Plugins already loaded"); pluginLocations = [pluginLocations arrayByAddingObject:[self _bundledPluginsURL]]; NSMutableArray *loadedPlugins = [NSMutableArray array]; for (NSURL *pluginLocation in pluginLocations) { NSString *pluginPath = pluginLocation.path; NSArray *plugins = [self _loadPluginsAtPath:pluginPath]; if (plugins) { [loadedPlugins addObjectsFromArray:plugins]; } } self.loadedPlugins = loadedPlugins; self.pluginsLoaded = YES; [self _populateModules]; } - (nullable NSArray *)_loadPluginsAtPath:(NSString *)pluginsPath { NSParameterAssert(pluginsPath != nil); NSMutableArray *loadedPlugins = [NSMutableArray array]; NSError *listedFilesError; NSArray *listedFiles = [RZFileManager() contentsOfDirectoryAtPath:pluginsPath error:&listedFilesError]; if (listedFiles == nil) { LogToConsoleError("Failed to list plugins: %{public}@", listedFilesError.localizedDescription); return nil; } for (NSString *file in listedFiles) { if ([file hasSuffix:@".mediaPlugin"] == NO) { continue; } NSString *filePath = [pluginsPath stringByAppendingPathComponent:file]; NSBundle *bundle = [self _loadPluginAtPath:filePath]; if (bundle == nil) { continue; } [loadedPlugins addObject:bundle]; } return loadedPlugins; } - (nullable NSBundle *)_loadPluginAtPath:(NSString *)pluginPath { NSParameterAssert(pluginPath != nil); /* Load bundle */ NSBundle *bundle = [NSBundle bundleWithPath:pluginPath]; if (bundle == nil) { return nil; } /* Check for a principal class */ Class principalClass = bundle.principalClass; if (principalClass == NULL) { LogToConsoleError("Failed to load bundle '%{public}@' because of NULL principal class", bundle.bundleURL.standardizedTildePath); return nil; } /* Check for conformity */ if ([principalClass conformsToProtocol:@protocol(ICLPluginProtocol)] == NO) { LogToConsoleError("Failed to load bundle '%{public}@' because it does not conform to the ICLPluginProtocol protocol", bundle.bundleURL.standardizedTildePath); return nil; } /* Success */ return bundle; } - (void)_populateModules { NSArray *loadedPlugins = self.loadedPlugins; if (loadedPlugins.count == 0) { LogToConsoleInfo("No plugins to load modules from"); return; } NSMutableArray *loadedModules = [NSMutableArray array]; for (NSBundle *plugin in loadedPlugins) { NSArray *modules = [self _populateModulesForPlugin:plugin]; [loadedModules addObjectsFromArray:modules]; } self.loadedModules = loadedModules; } - (NSArray *)_populateModulesForPlugin:(NSBundle *)plugin { NSParameterAssert(plugin != nil); /* We have already proven in -_loadPluginAtPath: that the plugin conforms to everything so we don't have to perform validation. */ Class principalClass = plugin.principalClass; return [principalClass performSelector:@selector(modules)]; } - (NSArray *)modules { NSArray *modules = self.loadedModules; if (modules == nil) { return @[]; } return modules; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLProcessDelegate.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentProtocol.h" #import "ICLProcessMainPrivate.h" #import "ICLProcessDelegatePrivate.h" NS_ASSUME_NONNULL_BEGIN @implementation ICLProcessDelegate #pragma mark - #pragma mark XPC Delegate - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { NSXPCInterface *exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ICLInlineContentServerProtocol)]; [exportedInterface setClasses:[NSSet setWithObjects:[NSArray class], [NSURL class], nil] forSelector:@selector(warmServiceByLoadingPluginsAtLocations:) argumentIndex:0 ofReply:NO]; newConnection.exportedInterface = exportedInterface; ICLProcessMain *exportedObject = [[ICLProcessMain alloc] initWithXPCConnection:newConnection]; newConnection.exportedObject = exportedObject; NSXPCInterface *remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ICLInlineContentClientProtocol)]; newConnection.remoteObjectInterface = remoteObjectInterface; [newConnection resume]; return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/ICLProcessMain.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #import "NSObjectHelperPrivate.h" #import "TPCPreferences.h" #import "TPCPreferencesUserDefaults.h" #import "ICLInlineContentModulePrivate.h" #import "ICLPayloadPrivate.h" #import "ICLPluginManagerPrivate.h" #import "CoreModulesImportsPrivate.h" #import "ICLProcessMainPrivate.h" NS_ASSUME_NONNULL_BEGIN NSString * const ICLInlineContentErrorDomain = @"ICLInlineContentErrorDomain"; @interface ICLProcessMain () @property (nonatomic, strong) NSXPCConnection *serviceConnection; @property (readonly, copy) NSArray *moduleClasses; @property (readonly, copy) NSArray *moduleClassesInCore; @property (readonly, copy) NSDictionary *> *modules; @property (readonly) NSCache *moduleReferences; @end @implementation ICLProcessMain - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (instancetype)initWithXPCConnection:(NSXPCConnection *)connection { NSParameterAssert(connection != nil); if ((self = [super init])) { self.serviceConnection = connection; LogToConsoleSetDefaultSubsystemToMainBundle(@"General"); return self; } return nil; } #pragma mark - #pragma mark XPC Interface /* List of module classes that are automatically mapped */ - (NSArray *)moduleClassesInCore { return @[ /* This module should ALWAYS be the last in line because it matches any URL. */ [ICMAssessedMedia class] ]; } - (NSArray *)moduleClasses { NSArray *modules = [[ICLPluginManager sharedPluginManager] modules]; modules = [modules arrayByAddingObjectsFromArray:self.moduleClassesInCore]; return modules; } /* Returns a dictionary with the key equal to the domain a module maps to and the value a list of modules that map to that domain. */ /* If a module does not map to a specific domain, then those can be accessed by the wildcard key "*" */ - (NSDictionary *> *)modules { static NSDictionary *> *modules = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ /* Add new modules to this array */ NSArray *moduleClasses = self.moduleClasses; /* Mapping logic */ NSMutableDictionary *> *modulesOut = [NSMutableDictionary dictionary]; void (^mapModuleDomain)(Class, NSString *) = ^(Class moduleClass, NSString *moduleDomain) { NSMutableArray *mappedDomains = modulesOut[moduleDomain]; if (mappedDomains == nil) { mappedDomains = [NSMutableArray array]; modulesOut[moduleDomain] = mappedDomains; } [mappedDomains addObject:moduleClass]; }; void (^mapModuleDomains)(Class, NSArray *) = ^(Class moduleClass, NSArray * _Nullable moduleDomains) { /* If the module does not map to a specific domain, then map it to a wildcard for all other classes. */ if (moduleDomains == nil || moduleDomains.count == 0) { mapModuleDomain(moduleClass, @"*"); return; } /* Map domains */ for (NSString *moduleDomain in moduleDomains) { mapModuleDomain(moduleClass, moduleDomain); } }; for (Class moduleClass in moduleClasses) { NSArray *moduleDomains = [moduleClass domains]; mapModuleDomains(moduleClass, moduleDomains); } /* Replace mutable arrays with immutable copies */ [modulesOut performSelectorOnObjectValueAndReplace:@selector(copy)]; modules = [modulesOut copy]; }); return modules; } - (void)processURL:(NSURL *)url withUniqueIdentifier:(NSString *)uniqueIdentifier atLineNumber:(NSString *)lineNumber index:(NSUInteger)index inView:(NSString *)viewIdentifier { NSParameterAssert(url != nil); NSParameterAssert(url.isFileURL == NO); NSParameterAssert(uniqueIdentifier != nil); NSParameterAssert(lineNumber != nil); NSParameterAssert(viewIdentifier != nil); ICLPayloadMutable *payload = [[ICLPayloadMutable alloc] initWithURL:url withUniqueIdentifier:uniqueIdentifier atLineNumber:lineNumber index:index inView:viewIdentifier]; [self processPayload:payload]; } - (void)processPayload:(ICLPayload *)payload { NSParameterAssert(payload != nil); NSString *urlScheme = payload.url.scheme; if ([urlScheme isEqualToString:@"http"] == NO && [urlScheme isEqualToString:@"https"] == NO) { return; } ICLPayloadMutable *payloadIn = nil; if ([payloadIn isKindOfClass:[ICLPayloadMutable class]] == NO) { payloadIn = [payload mutableCopy]; } else { payloadIn = (id)payload; } BOOL (^processModulesWithDomain)(NSString *) = ^BOOL (NSString *domain) { NSParameterAssert(domain != nil); NSArray *modules = self.modules[domain]; for (Class module in modules) { if ([self _processPayload:payloadIn usingModule:module]) { return YES; } } return NO; }; NSString *urlHost = payloadIn.url.host; if (processModulesWithDomain(urlHost)) { return; } /* If no module accepted responsibility for the urlHost, then we try modules that do not map to a specific domain. */ (void)processModulesWithDomain(@"*"); } - (BOOL)_processPayload:(ICLPayloadMutable *)payloadIn usingModule:(Class)moduleClass { NSParameterAssert(payloadIn != nil); NSParameterAssert(moduleClass != NULL); /* Do not allow unsafe content */ if ([moduleClass contentImageOrVideo] == NO && [TPCPreferences inlineMediaLimitToBasics]) { return NO; } else if ([moduleClass contentIsFile] == NO && [TPCPreferences inlineMediaLimitToBasics] && [TPCPreferences inlineMediaLimitBasicsToFiles]) { return NO; } else if ([moduleClass contentNotSafeForWork] && [TPCPreferences inlineMediaLimitNaughtyContent]) { return NO; } else if ([moduleClass contentUntrusted] && [TPCPreferences inlineMediaLimitUnsafeContent]) { return NO; } /* Determine whether this module has an action for this URL. */ NSURL *url = payloadIn.url; ICLInlineContentModuleActionBlock actionBlock = [moduleClass actionBlockForURL:url]; SEL action = NULL; if (actionBlock == nil) { action = [moduleClass actionForURL:url]; } if (actionBlock == nil && (action == NULL /* || [moduleClass instancesRespondToSelector:action] == NO */)) { return NO; } /* Create module and call it */ ICLInlineContentModule *module = [[moduleClass alloc] initWithPayload:payloadIn inProcess:self]; [self _addReferenceForModule:module]; if (actionBlock) { actionBlock(module); } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [module performSelector:action]; #pragma clang diagnostic pop } return YES; } #pragma mark - #pragma mark State - (void)_finalizeModule:(ICLInlineContentModule *)module withError:(nullable NSError *)error { NSParameterAssert(module != nil); ICLPayload *payload = [module.payload copy]; /* Remove reference to module */ [self _removeReferenceForModule:module]; /* If you are wondering why so much care has been put into these errors when we control the code, it's because there are plans to support plugins for modules in the future so future proofing it is best. */ if (payload.html.length == 0 && payload.scriptResources.count == 0) { error = [NSError errorWithDomain:ICLInlineContentErrorDomain code:1001 userInfo:@{ NSLocalizedDescriptionKey : @"-[ICLPayload scriptResources] must contain at least one path if -[ICLPayload html] is empty" }]; } else if (payload.html.length == 0 && payload.entrypoint.length == 0) { error = [NSError errorWithDomain:ICLInlineContentErrorDomain code:1002 userInfo:@{ NSLocalizedDescriptionKey : @"-[ICLPayload html] and -[ICLPayload entrypoint] cannot both be empty" }]; } if (error) { [[self remoteObjectProxy] processingPayload:payload failedWithError:error]; } else { [[self remoteObjectProxy] processingPayloadSucceeded:payload]; } } - (void)_cancelModule:(ICLInlineContentModule *)module { NSParameterAssert(module != nil); [self _removeReferenceForModule:module]; } - (void)_deferModule:(ICLInlineContentModule *)module asType:(ICLMediaType)type performCheck:(BOOL)performCheck { NSParameterAssert(module != nil); NSParameterAssert(type == ICLMediaTypeImage || type == ICLMediaTypeVideo || type == ICLMediaTypeVideoGif); switch (type) { case ICLMediaTypeImage: { ICMInlineImage *imageModule = [[ICMInlineImage alloc] initWithDeferredModule:module]; [self _addReferenceForModule:imageModule]; [imageModule performActionWithImageCheck:performCheck]; break; } case ICLMediaTypeVideo: { ICMInlineVideo *videoModule = [[ICMInlineVideo alloc] initWithDeferredModule:module]; [self _addReferenceForModule:videoModule]; [videoModule performActionWithVideoCheck:performCheck]; break; } case ICLMediaTypeVideoGif: { ICMInlineGifVideo *videoModule = [[ICMInlineGifVideo alloc] initWithDeferredModule:module]; [self _addReferenceForModule:videoModule]; [videoModule performActionWithVideoCheck:performCheck]; break; } default: { LogToConsoleError("Unexpected media type: %{public}lu", type); break; } // case } // switch } #pragma mark - #pragma mark Memory - (NSCache *)moduleReferences { static NSCache *modules = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ modules = [NSCache new]; }); return modules; } - (void)_addReferenceForModule:(ICLInlineContentModule *)module { NSParameterAssert(module != nil); [self.moduleReferences setObject:module forKey:module.description]; } - (void)_removeReferenceForModule:(ICLInlineContentModule *)module { NSParameterAssert(module != nil); [self.moduleReferences removeObjectForKey:module.description]; } #pragma mark - #pragma mark Process Management - (void)warmServiceByLoadingPluginsAtLocations:(NSArray *)pluginLocations { NSParameterAssert(pluginLocations != nil); static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [[ICLPluginManager sharedPluginManager] loadPluginsAtLocations:pluginLocations]; }); } - (void)warmServiceByRegisteringDefaults:(NSDictionary *)defaults { NSParameterAssert(defaults != nil); static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [RZUserDefaults() registerDefaults:defaults]; }); } #pragma mark - #pragma mark XPC Connection - (id )remoteObjectProxy { return self.serviceConnection.remoteObjectProxy; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Classes/Service/main.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLProcessDelegatePrivate.h" NS_ASSUME_NONNULL_BEGIN #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnullability-inferred-on-nested-type" int main(int argc, const char *argv[]) { ICLProcessDelegate *delegate = [ICLProcessDelegate new]; NSXPCListener *listener = [NSXPCListener serviceListener]; listener.delegate = delegate; [listener resume]; return 0; } #pragma clang diagnostic pop NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Configurations/Sandbox/Release.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.application-groups 8482Q6EPL6.com.codeux.apps.textual com.apple.security.network.client ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Headers/Private/ICPCoreMediaPCHPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import #define NSBundleForClass() \ [NSBundle bundleForClass:self.class] ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Headers/Private/ICPCoreMediaPrivate.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLPluginProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface ICPCoreMedia : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/ICPCoreMedia.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICPCoreMediaPrivate.h" #import "ICMCommonInlineImages.h" #import "ICMCommonInlineVideos.h" #import "ICMDailymotion.h" #import "ICMGyazo.h" #import "ICMImgurGifv.h" #import "ICMPornhub.h" #import "ICMStreamable.h" #import "ICMTweet.h" #import "ICMTwitchClips.h" #import "ICMTwitchLive.h" #import "ICMVimeo.h" #import "ICMXkcd.h" #import "ICMYouTube.h" NS_ASSUME_NONNULL_BEGIN @implementation ICPCoreMedia + (NSArray *)modules { return @[ [ICMDailymotion class], [ICMGyazo class], [ICMImgurGifv class], [ICMPornhub class], [ICMStreamable class], [ICMTweet class], /* Twitch now requires a parent= argument when embedding content. This argument acts as the domain that the content will be embedded in the context of to allow security headers to be set. Textual is not a web server. It loads files using file:// scheme. Even using "localhost" will not allow embeds to work. Is embedding Twitch really worth the cost of hosting a local server to spoof a localhost? Probably not. */ // [ICMTwitchClips class], // [ICMTwitchLive class], [ICMVimeo class], [ICMXkcd class], [ICMYouTube class], [ICMCommonInlineVideos class], [ICMCommonInlineImages class] ]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Common Images/ICMCommonInlineImages.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineImage.h" NS_ASSUME_NONNULL_BEGIN @interface ICMCommonInlineImages : ICMInlineImage @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Common Images/ICMCommonInlineImages.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2020 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "TPCPreferences.h" #import "ICLHelpers.h" #import "ICMCommonInlineImages.h" NS_ASSUME_NONNULL_BEGIN @interface ICMCommonInlineImages () @property (readonly, copy, class) NSArray *validFileExtensions; @end @implementation ICMCommonInlineImages + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSString *address = [self _finalAddressForURL:url]; if (address == nil) { return nil; } return [super actionBlockForAddress:address]; } + (nullable NSString *)_finalAddressForURL:(NSURL *)url { NSString *urlHost = url.host; NSString *urlPath = url.path.percentEncodedURLPath; NSString *urlPathExtension = urlPath.pathExtension; BOOL hasFileExtension = [self.validFileExtensions containsObject:urlPathExtension]; if (hasFileExtension) { if ([urlHost isDomainOrSubdomain:@"wikipedia.org"]) { /* Wikipedia URLs end with a file extension but tend to be a web page. There was no easy way hotlink these images at the time this exception was added. This should be revisited at a later time... */ return nil; } else if ([urlHost isDomainOrSubdomain:@"dropbox.com"]) { /* Processed below */ } else { return url.absoluteString; } } NSString *urlScheme = url.scheme; NSString *urlQuery = url.query.percentEncodedURLQuery; NSString *urlPathCombined = urlPath; if (urlQuery) { urlPathCombined = [urlPathCombined stringByAppendingFormat:@"?%@", urlQuery]; } if ([urlHost isDomainOrSubdomain:@"dropbox.com"]) { if ([urlPathCombined hasPrefix:@"/s/"] && hasFileExtension) { return [@"https://dl.dropboxusercontent.com" stringByAppendingString:urlPathCombined]; } } else if ([urlHost isDomain:@"pbs.twimg.com"]) { if (urlPath.length == 0) { return nil; } urlPath = [urlPath stringByReplacingOccurrencesOfString:@"\\:(large|medium|orig|small|thumb)$" withString:@"" options:NSRegularExpressionSearch range:urlPath.range]; return [NSString stringWithFormat:@"https://pbs.twimg.com%@:orig", urlPath]; } else if ([urlHost isDomain:@"docs.google.com"]) { if ([urlPath hasPrefix:@"/file/d/"] == NO) { return nil; } NSString *photoId = nil; NSArray *components = [urlPath componentsSeparatedByString:@"/"]; if (components.count == 5) { if ([components[4] isEqualToString:@"edit"]) { // Add a little validation photoId = components[3]; } } else if (components.count == 4) { photoId = components[3]; } else { return nil; } if (photoId) { return [@"https://docs.google.com/uc?id=" stringByAppendingString:photoId]; } } else if ([urlHost isDomainOrSubdomain:@"twitpic.com"]) { if (urlPath.length == 0) { return nil; } NSString *s = [urlPath substringFromIndex:1]; if ([s hasSuffix:@"/full"]) { s = [s substringToIndex:(s.length - 5)]; } if (s.alphabeticNumericOnly) { return [NSString stringWithFormat:@"http://twitpic.com/show/large/%@", s]; } } else if ([urlHost isDomainOrSubdomain:@"instagram.com"] || [urlHost isDomainOrSubdomain:@"instagr.am"]) { if ([urlPath hasPrefix:@"/p/"] == NO) { return nil; } NSString *s = [urlPath substringFromIndex:3]; if ([s onlyContainsCharactersFromCharacterSet:[NSCharacterSet Ato9UnderscoreDash]]) { return [NSString stringWithFormat:@"https://www.instagram.com/p/%@/media/?size=l", s]; } } else if ([urlHost isDomainOrSubdomain:@"i.4cdn.org"]) { if ([urlPath hasSuffix:@".webm"] == NO) { return nil; } NSString *filenameWithoutExtension = urlPath.stringByDeletingPathExtension; return [NSString stringWithFormat:@"%@://%@%@s.jpg", urlScheme, urlHost, filenameWithoutExtension]; } else if ([urlHost isDomainOrSubdomain:@"8ch.net"]) { if ([urlPath hasSuffix:@".webm"] == NO) { return nil; } NSString *filename = urlPath.lastPathComponent; NSString *filenameWithoutExtension = filename.stringByDeletingPathExtension; return [NSString stringWithFormat:@"%@://%@/webm/thumb/%@.jpg", urlScheme, urlHost, filenameWithoutExtension]; } else if ([urlHost isDomainOrSubdomain:@"f.hatena.ne.jp"]) { NSArray *components = [urlPath componentsSeparatedByString:@"/"]; if (components.count < 3) { return nil; } NSString *userId = components[1]; NSString *photoId = components[2]; if (userId.length == 0 || photoId.length < 8) { return nil; } if (photoId.numericOnly == NO) { return nil; } NSString *userIdHead = [userId substringToIndex:1]; NSString *photoIdHead = [photoId substringToIndex:8]; return [NSString stringWithFormat:@"http://img.f.hatena.ne.jp/images/fotolife/%@/%@/%@/%@.jpg", userIdHead, userId, photoIdHead, photoId]; } else if ([urlHost isDomain:@"puu.sh"]) { if (urlPath.length == 0) { return nil; } NSString *s = [urlPath substringFromIndex:1]; if (s.alphabeticNumericOnly) { return [NSString stringWithFormat:@"http://puu.sh/%@.jpg", s]; } } else if ([urlHost isDomainOrSubdomain:@"d.pr"]) { if ([urlPath hasPrefix:@"/i/"] == NO) { return nil; } NSString *s = [urlPath substringFromIndex:3]; if (s.alphabeticNumericOnly) { return [NSString stringWithFormat:@"http://d.pr/i/%@.png", s]; } } else if ([urlHost isDomainOrSubdomain:@"nicovideo.jp"] || [urlHost isDomain:@"nico.ms"]) { if (urlPath.length == 0) { return nil; } NSString *videoId = nil; NSString *s = nil; if ([urlHost isDomain:@"nico.ms"]) { s = [urlPath substringFromIndex:1]; } else if ([urlPath hasPrefix:@"/watch/"]) { s = [urlPath substringFromIndex:7]; } if ([s hasPrefix:@"sm"] || [s hasPrefix:@"nm"]) { videoId = s; } if (videoId.length < 3) { return nil; } long long videoNumber = [videoId substringFromIndex:2].longLongValue; return [NSString stringWithFormat:@"http://tn-skr%lli.smilevideo.jp/smile?i=%lli", ((videoNumber % 4) + 1), videoNumber]; } else if ([urlHost isDomain:@"i.reddituploads.com"]) { if (urlPath.length == 0) { return nil; } NSString *s = [urlPath substringFromIndex:1]; if (s.alphabeticNumericOnly) { return url.absoluteString; } } else if ([urlHost isDomainOrSubdomain:@"youtube.com"] || [urlHost isDomain:@"youtu.be"]) { /* If we aren't allowed to embed YouTube, at least show show the thumbnail for the video. */ if ([TPCPreferences inlineMediaLimitBasicsToFiles] == NO) { return nil; } if (urlPath.length == 0) { return nil; } NSString *videoId = nil; if ([urlHost isDomain:@"youtu.be"]) { videoId = [urlPath substringFromIndex:1]; } else { NSDictionary *queryItems = urlQuery.URLQueryItems; videoId = queryItems[@"v"]; } if (videoId.length < 11) { return nil; } if (videoId.length > 11) { videoId = [videoId substringToIndex:11]; } return [NSString stringWithFormat:@"http://i.ytimg.com/vi/%@/mqdefault.jpg", videoId]; } else if ([urlHost isDomainOrSubdomain:@"speedtest.net"]) { NSArray *components = [urlPath componentsSeparatedByString:@"/"]; if (components.count < 3) { return nil; } if ([components[1] isEqualToString:@"result"] == NO) { return nil; } NSString *resultId = components[2]; if (resultId.numericOnly == NO) { return nil; } return [NSString stringWithFormat:@"http://www.speedtest.net/result/%@.png", resultId]; } else if ([urlHost isDomain:@"fuelrats.cloud"]) { if ([urlPath hasPrefix:@"/s/"] == NO) { return nil; } NSString *s = [urlPath substringFromIndex:3]; if (s.alphabeticNumericOnly) { return [NSString stringWithFormat:@"https://fuelrats.cloud/s/%@/preview", s]; } } return nil; } + (NSArray *)validFileExtensions { static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = @[@"jpg", @"jpeg", @"png", @"gif", @"tif", @"tiff", @"svg", @"bmp"]; }); return cachedValue; } + (BOOL)contentIsFile { return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Common Videos/ICMCommonInlineVideos.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMCommonInlineVideos : ICMInlineVideo @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Common Videos/ICMCommonInlineVideos.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLHelpers.h" #import "ICMCommonInlineVideos.h" NS_ASSUME_NONNULL_BEGIN @interface ICMCommonInlineVideos () @property (readonly, copy, class) NSArray *validFileExtensions; @end @implementation ICMCommonInlineVideos + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSString *address = [self _finalAddressForURL:url]; if (address == nil) { return nil; } return [super actionBlockForAddress:address]; } + (nullable NSString *)_finalAddressForURL:(NSURL *)url { NSString *urlHost = url.host; NSString *urlPath = url.path.percentEncodedURLPath; NSString *urlPathExtension = urlPath.pathExtension; BOOL hasFileExtension = [self.validFileExtensions containsObject:urlPathExtension]; if (hasFileExtension) { if ([urlHost isDomain:@"video.nest.com"]) { /* Processed below */ } else { return url.absoluteString; } } if ([urlHost isDomain:@"video.nest.com"]) { if ([urlPath hasPrefix:@"/clip/"] == NO) { return nil; } NSString *filename = urlPath.lastPathComponent; NSString *filenameWithoutExtension = filename.stringByDeletingPathExtension; if (filenameWithoutExtension.alphabeticNumericOnly) { return [NSString stringWithFormat:@"http://clips.dropcam.com/%@", filename]; } } return nil; } + (NSArray *)validFileExtensions { static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = @[@"mp4", @"mov", @"m4v", @"3gp", @"3g2"]; }); return cachedValue; } + (BOOL)contentIsFile { return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Dailymotion/ICMDailymotion.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMDailymotion : ICMInlineVideoFoundation @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Dailymotion/ICMDailymotion.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMDailymotion.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMDailymotion - (void)_performActionForVideo:(NSString *)videoIdentifier { NSParameterAssert(videoIdentifier != nil); ICLPayloadMutable *payload = self.payload; NSDictionary *templateAttributes = @{ @"uniqueIdentifier" : payload.uniqueIdentifier, @"videoIdentifier" : videoIdentifier }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); NSString *videoIdentifier = [self _videoIdentifierForURL:url]; if (videoIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMDailymotion *moduleTyped = (id)module; [moduleTyped _performActionForVideo:videoIdentifier]; } copy]; } + (nullable NSString *)_videoIdentifierForURL:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if ([urlPath hasPrefix:@"/video/"] == NO) { return nil; } urlPath = [urlPath substringFromIndex:7]; // "/video/" /* Cut after first underscore so that URLs such as: http://www.dailymotion.com/video/x19pvwt_the-fantastic-four-1994-unreleased-roger-corman_shortfilms Automatically translate to their parent: http://www.dailymotion.com/video/x19pvwt */ NSInteger underscorePosition = [urlPath stringPosition:@"_"]; if (underscorePosition > 0) { urlPath = [urlPath substringToIndex:underscorePosition]; } NSString *videoIdentifier = urlPath; if (videoIdentifier.isAlphabeticNumericOnly == NO) { return nil; } return videoIdentifier; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"dailymotion.com", @"www.dailymotion.com", @"mobile.dailymotion.com" ]; }); return domains; } #pragma mark - #pragma mark Utilities - (nullable NSURL *)templateURL { return [NSBundleForClass() URLForResource:@"ICMDailymotion" withExtension:@"mustache"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Gyazo/ICMGyazo.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLInlineContentModule.h" NS_ASSUME_NONNULL_BEGIN @interface ICMGyazo : ICLInlineContentModule @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Gyazo/ICMGyazo.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLHelpers.h" #import "ICMGyazo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMGyazo () @property (nonatomic, copy) NSString *contentIdentifier; @end @implementation ICMGyazo - (void)_loadContent { NSString *contentAddress = self.payload.address; NSURLComponents *requestComponents = [NSURLComponents componentsWithString:@"https://api.gyazo.com/api/oembed"]; requestComponents.queryItems = @[ [NSURLQueryItem queryItemWithName:@"url" value:contentAddress] ]; NSURL *requestURL = requestComponents.URL; [ICLHelpers requestJSONDataFromURL:requestURL completionBlock:^(BOOL success, NSDictionary *data) { if (success == NO) { [self _unsafeToLoadMedia]; return; } [self _processJSONData:data]; }]; } - (void)_processJSONData:(NSDictionary *)data { NSParameterAssert(data != nil); /* Get "type" */ NSString *typeString = data[@"type"]; if (typeString == nil || [typeString isKindOfClass:[NSString class]] == NO) { [self _unsafeToLoadMedia]; return; } /* Check "type" */ if ([typeString isEqualToString:@"photo"]) { [self _processJSONDataForImage:data]; } else if ([typeString isEqualToString:@"video"]) { [self _processJSONDataForVideo:data]; } else { [self _unsafeToLoadMedia]; } } - (void)_processJSONDataForImage:(NSDictionary *)data { NSParameterAssert(data != nil); /* Get "url" */ NSString *urlString = data[@"url"]; if (urlString == nil || [urlString isKindOfClass:[NSString class]] == NO) { [self _unsafeToLoadMedia]; return; } /* Check "url" */ NSURL *url = [ICLHelpers URLWithString:urlString]; if (url == nil) { [self _unsafeToLoadMedia]; return; } /* Finish */ [self _safeToLoadMediaOfType:ICLMediaTypeImage atURL:url]; } - (void)_processJSONDataForVideo:(NSDictionary *)data { NSParameterAssert(data != nil); /* Get "url" */ NSString *urlString = [NSString stringWithFormat:@"https://i.gyazo.com/%@.mp4", self.contentIdentifier]; /* Check "url" */ NSURL *url = [ICLHelpers URLWithString:urlString]; if (url == nil) { [self _unsafeToLoadMedia]; return; } /* Finish */ [self _safeToLoadMediaOfType:ICLMediaTypeVideoGif atURL:url]; } - (void)_safeToLoadMediaOfType:(ICLMediaType)type atURL:(NSURL *)url { self.payload.urlToInline = url; [self deferAsType:type]; } - (void)_unsafeToLoadMedia { [self cancel]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); NSString *contentIdentifier = [self _contentIdentifierForURL:url]; if (contentIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMGyazo *moduleTyped = (id)module; moduleTyped.contentIdentifier = contentIdentifier; [moduleTyped _loadContent]; } copy]; } + (nullable NSString *)_contentIdentifierForURL:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length != 33) { // Includes leading slash return nil; } NSString *contentIdentifier = [urlPath substringFromIndex:1]; if (contentIdentifier.alphabeticNumericOnly == NO) { return nil; } return contentIdentifier; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"gyazo.com", @"www.gyazo.com" ]; }); return domains; } #pragma mark - #pragma mark Utilities + (BOOL)contentImageOrVideo { return YES; } + (BOOL)contentIsFile { return YES; } - (void)finalizePreflight { self.payload.classAttribute = @"inlineGyazo"; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Imgur .gifv/ICMImgurGifv.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMImgurGifv : ICMInlineGifVideo @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Imgur .gifv/ICMImgurGifv.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMImgurGifv.h" NS_ASSUME_NONNULL_BEGIN @interface ICMImgurGifv () @property (readonly, copy, class) NSArray *validFileExtensions; @end @implementation ICMImgurGifv + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSString *address = [self _finalAddressForURL:url]; if (address == nil) { return nil; } return [super actionBlockForAddress:address]; } + (nullable NSString *)_finalAddressForURL:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length <= 1) { return nil; } urlPath = [urlPath substringFromIndex:1]; // "/" NSString *fileExtension = urlPath.pathExtension; if ([self.validFileExtensions containsObject:fileExtension] == NO) { return nil; } NSString *videoIdentifier = urlPath.stringByDeletingPathExtension; if (videoIdentifier.isAlphabeticNumericOnly == NO) { return nil; } return [NSString stringWithFormat:@"https://i.imgur.com/%@.mp4", videoIdentifier]; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"i.imgur.com" ]; }); return domains; } + (NSArray *)validFileExtensions { static NSArray *cachedValue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cachedValue = @[@"mp4", @"gif", @"gifv", @"webp"]; }); return cachedValue; } + (BOOL)contentIsFile { return YES; } - (void)finalizePreflight { self.payload.classAttribute = @"inlineImgurGifv"; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Pornhub/ICMPornhub.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMPornhub : ICMInlineVideoFoundation @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Pornhub/ICMPornhub.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMPornhub.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMPornhub - (void)_performActionForVideo:(NSString *)videoIdentifier { NSParameterAssert(videoIdentifier != nil); ICLPayloadMutable *payload = self.payload; NSDictionary *templateAttributes = @{ @"uniqueIdentifier" : payload.uniqueIdentifier, @"videoIdentifier" : videoIdentifier }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); NSString *videoIdentifier = [self _videoIdentifierForURL:url]; if (videoIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMPornhub *moduleTyped = (id)module; [moduleTyped _performActionForVideo:videoIdentifier]; } copy]; } + (nullable NSString *)_videoIdentifierForURL:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if ([urlPath hasPrefix:@"/view_video.php"] == NO) { return nil; } NSString *urlQuery = url.query.percentEncodedURLQuery; NSDictionary *queryItems = urlQuery.URLQueryItems; NSString *videoIdentifier = queryItems[@"viewkey"]; if (videoIdentifier.isAlphabeticNumericOnly == NO) { return nil; } return videoIdentifier; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"pornhub.com", @"www.pornhub.com", @"pornhubpremium.com", @"www.pornhubpremium.com" ]; }); return domains; } #pragma mark - #pragma mark Utilities - (nullable NSURL *)templateURL { return [NSBundleForClass() URLForResource:@"ICMPornhub" withExtension:@"mustache"]; } + (BOOL)contentNotSafeForWork { return YES; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Streamable/ICMStreamable.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMStreamable : ICMInlineVideo @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Streamable/ICMStreamable.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLHelpers.h" #import "ICMStreamable.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMStreamable - (void)_performActionForVideo:(NSString *)videoIdentifier { NSParameterAssert(videoIdentifier != nil); NSString *addressToRequest = [@"https://api.streamable.com/videos/" stringByAppendingString:videoIdentifier]; [ICLHelpers requestJSONObject:@"url" ofType:[NSString class] inHierarchy:@[@"files", @"mp4"] fromAddress:addressToRequest completionBlock:^(id object) { if (object == nil) { [self notifyUnsafeToLoadVideo]; return; } [self performActionForAddress:object]; }]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); NSString *videoIdentifier = [self _videoIdentifierForURL:url]; if (videoIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMStreamable *moduleTyped = (id)module; [moduleTyped _performActionForVideo:videoIdentifier]; } copy]; } + (nullable NSString *)_videoIdentifierForURL:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length <= 1) { return nil; } NSString *videoIdentifier = [urlPath trimCharacters:@"/"]; if (videoIdentifier.isAlphabeticNumericOnly == NO) { return nil; } return videoIdentifier; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"streamable.com", @"www.streamable.com" ]; }); return domains; } + (BOOL)contentIsFile { return YES; } #pragma mark - #pragma mark Utilities - (void)finalizePreflight { self.payload.classAttribute = @"inlineStreamable"; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Twitch Clips/ICMTwitchClips.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMTwitchClips : ICMInlineVideoFoundation @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Twitch Clips/ICMTwitchClips.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMTwitchClips.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMTwitchClips - (void)_performActionForVideo:(NSString *)videoIdentifier { NSParameterAssert(videoIdentifier != nil); ICLPayloadMutable *payload = self.payload; NSDictionary *templateAttributes = @{ @"uniqueIdentifier" : payload.uniqueIdentifier, @"videoIdentifier" : videoIdentifier }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); NSString *videoIdentifier = [self _videoIdentifierForURL:url]; if (videoIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMTwitchClips *moduleTyped = (id)module; [moduleTyped _performActionForVideo:videoIdentifier]; } copy]; } + (nullable NSString *)_videoIdentifierForURL:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length <= 1) { return nil; } urlPath = [urlPath substringFromIndex:1]; // "/" // old: /username/NameOfClip // new: /NameOfClip // /NameOfClip/edit if ([urlPath hasSuffix:@"/edit"]) { urlPath = [urlPath substringToIndex:(urlPath.length - 5)]; // "/edit" } NSString *videoIdentifier = urlPath; if ([videoIdentifier onlyContainsCharactersFromCharacterSet: [NSCharacterSet Ato9UnderscoreDashForwardSlash]] == NO) { return nil; } return videoIdentifier; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"clips.twitch.tv" ]; }); return domains; } #pragma mark - #pragma mark Utilities - (nullable NSURL *)templateURL { return [NSBundleForClass() URLForResource:@"ICMTwitchClips" withExtension:@"mustache"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Twitch Live/ICMTwitchLive.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMTwitchLive : ICMInlineVideoFoundation @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Twitch Live/ICMTwitchLive.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMTwitchLive.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, ICMTwitchLiveContentTypeContent) { ICMTwitchLiveContentTypeUnknown = 0, ICMTwitchLiveContentTypeChannel, ICMTwitchLiveContentTypeVideo }; @implementation ICMTwitchLive - (void)_performActionForContent:(NSString *)contentIdentifier type:(ICMTwitchLiveContentTypeContent)contentType { NSParameterAssert(contentIdentifier != nil); NSParameterAssert(contentType == ICMTwitchLiveContentTypeChannel || contentType == ICMTwitchLiveContentTypeVideo); NSString *contentArgument = nil; if (contentType == ICMTwitchLiveContentTypeChannel) { contentArgument = @"channel"; } else if (contentType == ICMTwitchLiveContentTypeVideo) { contentArgument = @"video"; } ICLPayloadMutable *payload = self.payload; NSDictionary *templateAttributes = @{ @"uniqueIdentifier" : payload.uniqueIdentifier, @"contentIdentifier" : contentIdentifier, @"contentArgument" : contentArgument }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); ICMTwitchLiveContentTypeContent contentType = ICMTwitchLiveContentTypeUnknown; NSString *contentIdentifier = [self _contentIdentifierForURL:url type:&contentType]; if (contentIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMTwitchLive *moduleTyped = (id)module; [moduleTyped _performActionForContent:contentIdentifier type:contentType]; } copy]; } + (nullable NSString *)_contentIdentifierForURL:(NSURL *)url type:(ICMTwitchLiveContentTypeContent *)contentTypeIn { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length <= 1) { return nil; } urlPath = [urlPath substringFromIndex:1]; // "/" /* These exceptions cover all domains */ if ([urlPath isEqualToString:@"directory"] || [urlPath hasPrefix:@"directory/"] || [urlPath isEqualToString:@"store"] || [urlPath hasPrefix:@"store/"]) { return nil; } /* Match videos */ if ([urlPath hasPrefix:@"videos/"]) { urlPath = [urlPath substringFromIndex:7]; NSString *contentIdentifier = [urlPath trimCharacters:@"/"]; if (contentIdentifier.isNumericOnly == NO) { return nil; } *contentTypeIn = ICMTwitchLiveContentTypeVideo; return contentIdentifier; } /* Consider any other match a channel */ { NSString *contentIdentifier = [urlPath trimCharacters:@"/"]; if (contentIdentifier.length < 4 || contentIdentifier.length > 25) { return nil; } if ([contentIdentifier onlyContainsCharactersFromCharacterSet:[NSCharacterSet Ato9Underscore]] == NO) { return nil; } *contentTypeIn = ICMTwitchLiveContentTypeChannel; return contentIdentifier; } } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"twitch.tv", @"www.twitch.tv", @"go.twitch.tv" ]; }); return domains; } #pragma mark - #pragma mark Utilities - (nullable NSURL *)templateURL { return [NSBundleForClass() URLForResource:@"ICMTwitchLive" withExtension:@"mustache"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Twitter/ICMTweet.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineHTML.h" NS_ASSUME_NONNULL_BEGIN @interface ICMTweet : ICMInlineHTML @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Twitter/ICMTweet.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLHelpers.h" #import "ICMTweet.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMTweet - (void)_loadTweetContents { NSString *tweetAddress = self.payload.address; NSURLComponents *requestComponents = [NSURLComponents componentsWithString:@"https://publish.twitter.com/oembed"]; requestComponents.queryItems = @[ [NSURLQueryItem queryItemWithName:@"dnt" value:@"true"], /* DO NOT TRACK */ [NSURLQueryItem queryItemWithName:@"maxwidth" value:@"500"], [NSURLQueryItem queryItemWithName:@"omit_script" value:@"true"], [NSURLQueryItem queryItemWithName:@"url" value:tweetAddress] ]; NSURL *requestURL = requestComponents.URL; [ICLHelpers requestJSONObject:@"html" ofType:[NSString class] inHierarchy:nil fromURL:requestURL completionBlock:^(id object) { if (object == nil) { [self notifyUnableToPresentHTML]; return; } [self performActionForHTML:object]; }]; } #pragma mark - #pragma mark Action Block + (nullable SEL)actionForURL:(NSURL *)url { NSParameterAssert(url != nil); if ([self _URLIsTweet:url] == NO) { return NULL; } return @selector(_loadTweetContents); } + (BOOL)_URLIsTweet:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length <= 1) { return NO; } urlPath = [urlPath substringFromIndex:1]; // "/" NSArray *components = [urlPath componentsSeparatedByString:@"/"]; if (components.count < 3) { return NO; } if ([components[1] isEqualToString:@"status"] == NO) { return NO; } if (components[2].isNumericOnly == NO) { return NO; } return YES; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"twitter.com", @"www.twitter.com", @"mobile.twitter.com" ]; }); return domains; } #pragma mark - #pragma mark Utilities - (nullable NSArray *)scriptResources { return [[super scriptResources] arrayByAddingObjectsFromArray: @[ [NSURL URLWithString:@"https://platform.twitter.com/widgets.js"], [NSBundleForClass() URLForResource:@"ICMTweet" withExtension:@"js"] ]]; } - (nullable NSString *)entrypoint { return @"_ICMTweet"; } - (void)finalizePreflight { self.payload.classAttribute = @"inlineTweet"; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Vimeo/ICMVimeo.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMVimeo : ICMInlineVideoFoundation @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/Vimeo/ICMVimeo.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMVimeo.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMVimeo - (void)_performActionForVideo:(NSString *)videoIdentifier { NSParameterAssert(videoIdentifier != nil); ICLPayloadMutable *payload = self.payload; NSDictionary *templateAttributes = @{ @"uniqueIdentifier" : payload.uniqueIdentifier, @"videoIdentifier" : videoIdentifier }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); NSString *videoIdentifier = [self _videoIdentifierForURL:url]; if (videoIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMVimeo *moduleTyped = (id)module; [moduleTyped _performActionForVideo:videoIdentifier]; } copy]; } + (nullable NSString *)_videoIdentifierForURL:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length <= 1) { return nil; } NSString *videoIdentifier = [urlPath trimCharacters:@"/"]; if (videoIdentifier.isNumericOnly == NO) { return nil; } return videoIdentifier; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"vimeo.com", @"www.vimeo.com" ]; }); return domains; } #pragma mark - #pragma mark Utilities - (nullable NSURL *)templateURL { return [NSBundleForClass() URLForResource:@"ICMVimeo" withExtension:@"mustache"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/YouTube/ICMYouTube.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineVideo.h" NS_ASSUME_NONNULL_BEGIN @interface ICMYouTube : ICMInlineVideoFoundation @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/YouTube/ICMYouTube.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLHelpers.h" #import "ICMYouTube.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMYouTube - (void)_performActionForVideo:(NSString *)videoIdentifier { NSParameterAssert(videoIdentifier != nil); ICLPayloadMutable *payload = self.payload; NSDictionary *templateAttributes = @{ @"uniqueIdentifier" : payload.uniqueIdentifier, @"videoIdentifier" : videoIdentifier, @"videoStartTime" : @(self.videoStartTime) }; NSError *templateRenderError = nil; NSString *html = [self.template renderObject:templateAttributes error:&templateRenderError]; payload.html = html; [self finalizeWithError:templateRenderError]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); NSTimeInterval startPosition = 0; NSString *videoIdentifier = [self _videoIdentifierForURL:url startPosition:&startPosition]; if (videoIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMYouTube *moduleTyped = (id)module; moduleTyped.videoStartTime = startPosition; [moduleTyped _performActionForVideo:videoIdentifier]; } copy]; } + (nullable NSString *)_videoIdentifierForURL:(NSURL *)url startPosition:(NSTimeInterval *)startPosition { NSString *videoIdentifier = nil; NSString *urlHost = url.host; NSString *urlQuery = url.query.percentEncodedURLQuery; NSDictionary *queryItems = urlQuery.URLQueryItems; if ([urlHost isEqualToString:@"youtu.be"]) { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length <= 1) { return nil; } videoIdentifier = [urlPath substringFromIndex:1]; } else if ([urlHost isDomainOrSubdomain:@"youtube.com"]) { NSString *urlPath = url.path.percentEncodedURLPath; if ([urlPath isEqualToString:@"/watch"] == NO) { return nil; } videoIdentifier = queryItems[@"v"]; } if (videoIdentifier.length < 11) { return nil; } if (videoIdentifier.length > 11) { videoIdentifier = [videoIdentifier substringToIndex:11]; } NSString *timestamp = queryItems[@"t"]; if (timestamp) { *startPosition = [self parseYouTubeEsqueTimestamp:timestamp]; } return videoIdentifier; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"youtube.com", @"www.youtube.com", @"m.youtube.com", @"youtu.be" ]; }); return domains; } #pragma mark - #pragma mark Utilities - (nullable NSURL *)templateURL { return [NSBundleForClass() URLForResource:@"ICMYouTube" withExtension:@"mustache"]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/xkcd/ICMXkcd.h ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICMInlineImage.h" NS_ASSUME_NONNULL_BEGIN @interface ICMXkcd : ICMInlineImage @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Classes/Modules/xkcd/ICMXkcd.m ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ #import "ICLHelpers.h" #import "ICMXkcd.h" NS_ASSUME_NONNULL_BEGIN @implementation ICMXkcd - (void)_performActionForComic:(NSString *)comicIdentifier { NSParameterAssert(comicIdentifier != nil); NSString *addressToRequest = [NSString stringWithFormat:@"https://xkcd.com/%@/info.0.json", comicIdentifier]; [ICLHelpers requestJSONObject:@"img" ofType:[NSString class] inHierarchy:nil fromAddress:addressToRequest completionBlock:^(id object) { if (object == nil) { [self notifyUnsafeToLoadImage]; return; } [self performActionForAddress:object]; }]; } #pragma mark - #pragma mark Action Block + (nullable ICLInlineContentModuleActionBlock)actionBlockForURL:(NSURL *)url { NSParameterAssert(url != nil); NSString *comicIdentifier = [self _comicIdentifierForURL:url]; if (comicIdentifier == nil) { return nil; } return [^(ICLInlineContentModule *module) { __weak ICMXkcd *moduleTyped = (id)module; [moduleTyped _performActionForComic:comicIdentifier]; } copy]; } + (nullable NSString *)_comicIdentifierForURL:(NSURL *)url { NSString *urlPath = url.path.percentEncodedURLPath; if (urlPath.length <= 1) { return nil; } NSString *comicIdentifier = [urlPath trimCharacters:@"/"]; if (comicIdentifier.isNumericOnly == NO) { return nil; } return comicIdentifier; } + (nullable NSArray *)domains { static NSArray *domains = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ domains = @[ @"xkcd.com", @"www.xkcd.com" ]; }); return domains; } + (BOOL)contentIsFile { return YES; } #pragma mark - #pragma mark Utilities - (void)finalizePreflight { self.payload.classAttribute = @"inlineXkcd"; } @end NS_ASSUME_NONNULL_END ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Inline Content Loader Core Media.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C469ED520EC600900094EA4 /* ICPCoreMedia.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EA620EC600900094EA4 /* ICPCoreMedia.m */; }; 4C469ED620EC600900094EA4 /* ICMCommonInlineVideos.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EA920EC600900094EA4 /* ICMCommonInlineVideos.m */; }; 4C469ED720EC600900094EA4 /* ICMTweet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EAC20EC600900094EA4 /* ICMTweet.m */; }; 4C469EDA20EC600900094EA4 /* ICMGyazo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EB620EC600900094EA4 /* ICMGyazo.m */; }; 4C469EDB20EC600900094EA4 /* ICMStreamable.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EB920EC600900094EA4 /* ICMStreamable.m */; }; 4C469EDC20EC600900094EA4 /* ICMVimeo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EBC20EC600900094EA4 /* ICMVimeo.m */; }; 4C469EDD20EC600900094EA4 /* ICMPornhub.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EBE20EC600900094EA4 /* ICMPornhub.m */; }; 4C469EDE20EC600900094EA4 /* ICMImgurGifv.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EC120EC600900094EA4 /* ICMImgurGifv.m */; }; 4C469EDF20EC600900094EA4 /* ICMYouTube.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EC420EC600900094EA4 /* ICMYouTube.m */; }; 4C469EE020EC600900094EA4 /* ICMDailymotion.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469EC820EC600900094EA4 /* ICMDailymotion.m */; }; 4C469EE120EC600900094EA4 /* ICMTwitchClips.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469ECA20EC600900094EA4 /* ICMTwitchClips.m */; }; 4C469EE220EC600900094EA4 /* ICMTwitchLive.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469ECD20EC600900094EA4 /* ICMTwitchLive.m */; }; 4C469EE320EC600900094EA4 /* ICMCommonInlineImages.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469ED020EC600900094EA4 /* ICMCommonInlineImages.m */; }; 4C469EE420EC600900094EA4 /* ICMXkcd.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469ED320EC600900094EA4 /* ICMXkcd.m */; }; 4C469F3720EC608E00094EA4 /* ICMTweet.js in Resources */ = {isa = PBXBuildFile; fileRef = 4C469F2620EC608E00094EA4 /* ICMTweet.js */; }; 4C469F3920EC608E00094EA4 /* ICMVimeo.mustache in Resources */ = {isa = PBXBuildFile; fileRef = 4C469F2A20EC608E00094EA4 /* ICMVimeo.mustache */; }; 4C469F3A20EC608E00094EA4 /* ICMPornhub.mustache in Resources */ = {isa = PBXBuildFile; fileRef = 4C469F2C20EC608E00094EA4 /* ICMPornhub.mustache */; }; 4C469F3B20EC608E00094EA4 /* ICMYouTube.mustache in Resources */ = {isa = PBXBuildFile; fileRef = 4C469F2E20EC608E00094EA4 /* ICMYouTube.mustache */; }; 4C469F3C20EC608E00094EA4 /* ICMDailymotion.mustache in Resources */ = {isa = PBXBuildFile; fileRef = 4C469F3020EC608E00094EA4 /* ICMDailymotion.mustache */; }; 4C469F3D20EC608E00094EA4 /* ICMTwitchClips.mustache in Resources */ = {isa = PBXBuildFile; fileRef = 4C469F3220EC608E00094EA4 /* ICMTwitchClips.mustache */; }; 4C469F3E20EC608E00094EA4 /* ICMTwitchLive.mustache in Resources */ = {isa = PBXBuildFile; fileRef = 4C469F3420EC608E00094EA4 /* ICMTwitchLive.mustache */; }; 4CE144E820EC0448000E01C9 /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CE144E720EC0448000E01C9 /* CocoaExtensions.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 4C469EA420EC600900094EA4 /* ICPCoreMediaPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICPCoreMediaPrivate.h; sourceTree = ""; }; 4C469EA520EC600900094EA4 /* ICPCoreMediaPCHPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICPCoreMediaPCHPrivate.h; sourceTree = ""; }; 4C469EA620EC600900094EA4 /* ICPCoreMedia.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICPCoreMedia.m; sourceTree = ""; }; 4C469EA920EC600900094EA4 /* ICMCommonInlineVideos.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMCommonInlineVideos.m; sourceTree = ""; }; 4C469EAA20EC600900094EA4 /* ICMCommonInlineVideos.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMCommonInlineVideos.h; sourceTree = ""; }; 4C469EAC20EC600900094EA4 /* ICMTweet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMTweet.m; sourceTree = ""; }; 4C469EAD20EC600900094EA4 /* ICMTweet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMTweet.h; sourceTree = ""; }; 4C469EB520EC600900094EA4 /* ICMGyazo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMGyazo.h; sourceTree = ""; }; 4C469EB620EC600900094EA4 /* ICMGyazo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMGyazo.m; sourceTree = ""; }; 4C469EB820EC600900094EA4 /* ICMStreamable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMStreamable.h; sourceTree = ""; }; 4C469EB920EC600900094EA4 /* ICMStreamable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMStreamable.m; sourceTree = ""; }; 4C469EBB20EC600900094EA4 /* ICMVimeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMVimeo.h; sourceTree = ""; }; 4C469EBC20EC600900094EA4 /* ICMVimeo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMVimeo.m; sourceTree = ""; }; 4C469EBE20EC600900094EA4 /* ICMPornhub.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMPornhub.m; sourceTree = ""; }; 4C469EBF20EC600900094EA4 /* ICMPornhub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMPornhub.h; sourceTree = ""; }; 4C469EC120EC600900094EA4 /* ICMImgurGifv.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMImgurGifv.m; sourceTree = ""; }; 4C469EC220EC600900094EA4 /* ICMImgurGifv.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMImgurGifv.h; sourceTree = ""; }; 4C469EC420EC600900094EA4 /* ICMYouTube.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMYouTube.m; sourceTree = ""; }; 4C469EC520EC600900094EA4 /* ICMYouTube.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMYouTube.h; sourceTree = ""; }; 4C469EC720EC600900094EA4 /* ICMDailymotion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMDailymotion.h; sourceTree = ""; }; 4C469EC820EC600900094EA4 /* ICMDailymotion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMDailymotion.m; sourceTree = ""; }; 4C469ECA20EC600900094EA4 /* ICMTwitchClips.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMTwitchClips.m; sourceTree = ""; }; 4C469ECB20EC600900094EA4 /* ICMTwitchClips.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMTwitchClips.h; sourceTree = ""; }; 4C469ECD20EC600900094EA4 /* ICMTwitchLive.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMTwitchLive.m; sourceTree = ""; }; 4C469ECE20EC600900094EA4 /* ICMTwitchLive.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMTwitchLive.h; sourceTree = ""; }; 4C469ED020EC600900094EA4 /* ICMCommonInlineImages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMCommonInlineImages.m; sourceTree = ""; }; 4C469ED120EC600900094EA4 /* ICMCommonInlineImages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMCommonInlineImages.h; sourceTree = ""; }; 4C469ED320EC600900094EA4 /* ICMXkcd.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMXkcd.m; sourceTree = ""; }; 4C469ED420EC600900094EA4 /* ICMXkcd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMXkcd.h; sourceTree = ""; }; 4C469F0120EC601B00094EA4 /* TPCPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPCPreferences.h; sourceTree = ""; }; 4C469F2620EC608E00094EA4 /* ICMTweet.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ICMTweet.js; sourceTree = ""; }; 4C469F2A20EC608E00094EA4 /* ICMVimeo.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMVimeo.mustache; sourceTree = ""; }; 4C469F2C20EC608E00094EA4 /* ICMPornhub.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMPornhub.mustache; sourceTree = ""; }; 4C469F2E20EC608E00094EA4 /* ICMYouTube.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMYouTube.mustache; sourceTree = ""; }; 4C469F3020EC608E00094EA4 /* ICMDailymotion.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMDailymotion.mustache; sourceTree = ""; }; 4C469F3220EC608E00094EA4 /* ICMTwitchClips.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMTwitchClips.mustache; sourceTree = ""; }; 4C469F3420EC608E00094EA4 /* ICMTwitchLive.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMTwitchLive.mustache; sourceTree = ""; }; 4C469F3620EC608E00094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4CA1FE581FA606890095901F /* Core Media.mediaPlugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Core Media.mediaPlugin"; sourceTree = BUILT_PRODUCTS_DIR; }; 4CE144E720EC0448000E01C9 /* CocoaExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaExtensions.framework; path = "../../../../.tmp/SharedBuildProducts-Frameworks/CocoaExtensions.framework"; sourceTree = SOURCE_ROOT; }; 4CE145B220EC0D5A000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE145B620EC0D5A000E01C9 /* XPC Service - ICL Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL Extensions.xcconfig"; sourceTree = ""; }; 4CE145B720EC0D5A000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE145C520EC0D5A000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE145C620EC0D5A000E01C9 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4CE145C920EC0D5A000E01C9 /* Foundation.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4CE145CB20EC0D5A000E01C9 /* XPC Service - ICL Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL Extensions.xcconfig"; sourceTree = ""; }; 4CE145CC20EC0D5A000E01C9 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4CE145DA20EC0D5A000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE145DE20EC0D5A000E01C9 /* XPC Service - ICL Extensions.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL Extensions.xcconfig"; sourceTree = ""; }; 4CE145DF20EC0D5A000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 4CA1FE551FA606890095901F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4CE144E820EC0448000E01C9 /* CocoaExtensions.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4C03B65B1FA6518A00748FE7 /* System Frameworks */ = { isa = PBXGroup; children = ( ); name = "System Frameworks"; sourceTree = ""; }; 4C03B6731FA6566200748FE7 /* External Frameworks */ = { isa = PBXGroup; children = ( 4CE144E720EC0448000E01C9 /* CocoaExtensions.framework */, ); name = "External Frameworks"; sourceTree = ""; }; 4C469EA120EC600900094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C469EA220EC600900094EA4 /* Headers */, 4C469EA720EC600900094EA4 /* Modules */, 4C469EE520EC601B00094EA4 /* Shared */, 4C469EA620EC600900094EA4 /* ICPCoreMedia.m */, ); path = Classes; sourceTree = ""; }; 4C469EA220EC600900094EA4 /* Headers */ = { isa = PBXGroup; children = ( 4C469EA320EC600900094EA4 /* Private */, ); path = Headers; sourceTree = ""; }; 4C469EA320EC600900094EA4 /* Private */ = { isa = PBXGroup; children = ( 4C469EA420EC600900094EA4 /* ICPCoreMediaPrivate.h */, 4C469EA520EC600900094EA4 /* ICPCoreMediaPCHPrivate.h */, ); path = Private; sourceTree = ""; }; 4C469EA720EC600900094EA4 /* Modules */ = { isa = PBXGroup; children = ( 4C469ECF20EC600900094EA4 /* Common Images */, 4C469EA820EC600900094EA4 /* Common Videos */, 4C469EC620EC600900094EA4 /* Dailymotion */, 4C469EB420EC600900094EA4 /* Gyazo */, 4C469EC020EC600900094EA4 /* Imgur .gifv */, 4C469EBD20EC600900094EA4 /* Pornhub */, 4C469EB720EC600900094EA4 /* Streamable */, 4C469EC920EC600900094EA4 /* Twitch Clips */, 4C469ECC20EC600900094EA4 /* Twitch Live */, 4C469EAB20EC600900094EA4 /* Twitter */, 4C469EBA20EC600900094EA4 /* Vimeo */, 4C469ED220EC600900094EA4 /* xkcd */, 4C469EC320EC600900094EA4 /* YouTube */, ); path = Modules; sourceTree = ""; }; 4C469EA820EC600900094EA4 /* Common Videos */ = { isa = PBXGroup; children = ( 4C469EA920EC600900094EA4 /* ICMCommonInlineVideos.m */, 4C469EAA20EC600900094EA4 /* ICMCommonInlineVideos.h */, ); path = "Common Videos"; sourceTree = ""; }; 4C469EAB20EC600900094EA4 /* Twitter */ = { isa = PBXGroup; children = ( 4C469EAC20EC600900094EA4 /* ICMTweet.m */, 4C469EAD20EC600900094EA4 /* ICMTweet.h */, ); path = Twitter; sourceTree = ""; }; 4C469EB420EC600900094EA4 /* Gyazo */ = { isa = PBXGroup; children = ( 4C469EB520EC600900094EA4 /* ICMGyazo.h */, 4C469EB620EC600900094EA4 /* ICMGyazo.m */, ); path = Gyazo; sourceTree = ""; }; 4C469EB720EC600900094EA4 /* Streamable */ = { isa = PBXGroup; children = ( 4C469EB820EC600900094EA4 /* ICMStreamable.h */, 4C469EB920EC600900094EA4 /* ICMStreamable.m */, ); path = Streamable; sourceTree = ""; }; 4C469EBA20EC600900094EA4 /* Vimeo */ = { isa = PBXGroup; children = ( 4C469EBB20EC600900094EA4 /* ICMVimeo.h */, 4C469EBC20EC600900094EA4 /* ICMVimeo.m */, ); path = Vimeo; sourceTree = ""; }; 4C469EBD20EC600900094EA4 /* Pornhub */ = { isa = PBXGroup; children = ( 4C469EBE20EC600900094EA4 /* ICMPornhub.m */, 4C469EBF20EC600900094EA4 /* ICMPornhub.h */, ); path = Pornhub; sourceTree = ""; }; 4C469EC020EC600900094EA4 /* Imgur .gifv */ = { isa = PBXGroup; children = ( 4C469EC120EC600900094EA4 /* ICMImgurGifv.m */, 4C469EC220EC600900094EA4 /* ICMImgurGifv.h */, ); path = "Imgur .gifv"; sourceTree = ""; }; 4C469EC320EC600900094EA4 /* YouTube */ = { isa = PBXGroup; children = ( 4C469EC420EC600900094EA4 /* ICMYouTube.m */, 4C469EC520EC600900094EA4 /* ICMYouTube.h */, ); path = YouTube; sourceTree = ""; }; 4C469EC620EC600900094EA4 /* Dailymotion */ = { isa = PBXGroup; children = ( 4C469EC720EC600900094EA4 /* ICMDailymotion.h */, 4C469EC820EC600900094EA4 /* ICMDailymotion.m */, ); path = Dailymotion; sourceTree = ""; }; 4C469EC920EC600900094EA4 /* Twitch Clips */ = { isa = PBXGroup; children = ( 4C469ECA20EC600900094EA4 /* ICMTwitchClips.m */, 4C469ECB20EC600900094EA4 /* ICMTwitchClips.h */, ); path = "Twitch Clips"; sourceTree = ""; }; 4C469ECC20EC600900094EA4 /* Twitch Live */ = { isa = PBXGroup; children = ( 4C469ECD20EC600900094EA4 /* ICMTwitchLive.m */, 4C469ECE20EC600900094EA4 /* ICMTwitchLive.h */, ); path = "Twitch Live"; sourceTree = ""; }; 4C469ECF20EC600900094EA4 /* Common Images */ = { isa = PBXGroup; children = ( 4C469ED020EC600900094EA4 /* ICMCommonInlineImages.m */, 4C469ED120EC600900094EA4 /* ICMCommonInlineImages.h */, ); path = "Common Images"; sourceTree = ""; }; 4C469ED220EC600900094EA4 /* xkcd */ = { isa = PBXGroup; children = ( 4C469ED320EC600900094EA4 /* ICMXkcd.m */, 4C469ED420EC600900094EA4 /* ICMXkcd.h */, ); path = xkcd; sourceTree = ""; }; 4C469EE520EC601B00094EA4 /* Shared */ = { isa = PBXGroup; children = ( 4C469EF420EC601B00094EA4 /* Headers */, ); name = Shared; path = ../../../../Sources/Shared; sourceTree = SOURCE_ROOT; }; 4C469EF420EC601B00094EA4 /* Headers */ = { isa = PBXGroup; children = ( 4C469F0120EC601B00094EA4 /* TPCPreferences.h */, ); path = Headers; sourceTree = ""; }; 4C469F2420EC608E00094EA4 /* Modules */ = { isa = PBXGroup; children = ( 4C469F2520EC608E00094EA4 /* Twitter */, 4C469F2920EC608E00094EA4 /* Vimeo */, 4C469F2B20EC608E00094EA4 /* Pornhub */, 4C469F2D20EC608E00094EA4 /* YouTube */, 4C469F2F20EC608E00094EA4 /* Dailymotion */, 4C469F3120EC608E00094EA4 /* Twitch Clips */, 4C469F3320EC608E00094EA4 /* Twitch Live */, ); path = Modules; sourceTree = ""; }; 4C469F2520EC608E00094EA4 /* Twitter */ = { isa = PBXGroup; children = ( 4C469F2620EC608E00094EA4 /* ICMTweet.js */, ); path = Twitter; sourceTree = ""; }; 4C469F2920EC608E00094EA4 /* Vimeo */ = { isa = PBXGroup; children = ( 4C469F2A20EC608E00094EA4 /* ICMVimeo.mustache */, ); path = Vimeo; sourceTree = ""; }; 4C469F2B20EC608E00094EA4 /* Pornhub */ = { isa = PBXGroup; children = ( 4C469F2C20EC608E00094EA4 /* ICMPornhub.mustache */, ); path = Pornhub; sourceTree = ""; }; 4C469F2D20EC608E00094EA4 /* YouTube */ = { isa = PBXGroup; children = ( 4C469F2E20EC608E00094EA4 /* ICMYouTube.mustache */, ); path = YouTube; sourceTree = ""; }; 4C469F2F20EC608E00094EA4 /* Dailymotion */ = { isa = PBXGroup; children = ( 4C469F3020EC608E00094EA4 /* ICMDailymotion.mustache */, ); path = Dailymotion; sourceTree = ""; }; 4C469F3120EC608E00094EA4 /* Twitch Clips */ = { isa = PBXGroup; children = ( 4C469F3220EC608E00094EA4 /* ICMTwitchClips.mustache */, ); path = "Twitch Clips"; sourceTree = ""; }; 4C469F3320EC608E00094EA4 /* Twitch Live */ = { isa = PBXGroup; children = ( 4C469F3420EC608E00094EA4 /* ICMTwitchLive.mustache */, ); path = "Twitch Live"; sourceTree = ""; }; 4C469F3520EC608E00094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C469F3620EC608E00094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4CA1FE4F1FA606890095901F = { isa = PBXGroup; children = ( 4C469EA120EC600900094EA4 /* Classes */, 4CA1FE611FA6069D0095901F /* Resources */, 4CA1FE671FA608E70095901F /* Frameworks */, 4CA1FE591FA606890095901F /* Products */, ); sourceTree = ""; }; 4CA1FE591FA606890095901F /* Products */ = { isa = PBXGroup; children = ( 4CA1FE581FA606890095901F /* Core Media.mediaPlugin */, ); name = Products; sourceTree = ""; }; 4CA1FE611FA6069D0095901F /* Resources */ = { isa = PBXGroup; children = ( 4CA1FE621FA607390095901F /* Build Settings */, 4C469F2420EC608E00094EA4 /* Modules */, 4C469F3520EC608E00094EA4 /* Property Lists */, ); path = Resources; sourceTree = ""; }; 4CA1FE621FA607390095901F /* Build Settings */ = { isa = PBXGroup; children = ( 4CA1FE631FA607450095901F /* Configurations */, ); name = "Build Settings"; sourceTree = ""; }; 4CA1FE631FA607450095901F /* Configurations */ = { isa = PBXGroup; children = ( 4CE145AE20EC0D5A000E01C9 /* Build */, ); name = Configurations; path = ../../../../../Configurations; sourceTree = ""; }; 4CA1FE671FA608E70095901F /* Frameworks */ = { isa = PBXGroup; children = ( 4C03B6731FA6566200748FE7 /* External Frameworks */, 4C03B65B1FA6518A00748FE7 /* System Frameworks */, ); name = Frameworks; sourceTree = ""; }; 4CE145AE20EC0D5A000E01C9 /* Build */ = { isa = PBXGroup; children = ( 4CE145C220EC0D5A000E01C9 /* Common */, 4CE145D720EC0D5A000E01C9 /* Debug */, 4CE145AF20EC0D5A000E01C9 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4CE145AF20EC0D5A000E01C9 /* Standard Release */ = { isa = PBXGroup; children = ( 4CE145B720EC0D5A000E01C9 /* Enabled Features.xcconfig */, 4CE145B220EC0D5A000E01C9 /* Textual.xcconfig */, 4CE145B620EC0D5A000E01C9 /* XPC Service - ICL Extensions.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4CE145C220EC0D5A000E01C9 /* Common */ = { isa = PBXGroup; children = ( 4CE145CC20EC0D5A000E01C9 /* Foundation Debug.xcconfig */, 4CE145C920EC0D5A000E01C9 /* Foundation.xcconfig */, 4CE145C620EC0D5A000E01C9 /* Preserve Symbols.xcconfig */, 4CE145C520EC0D5A000E01C9 /* Textual.xcconfig */, 4CE145CB20EC0D5A000E01C9 /* XPC Service - ICL Extensions.xcconfig */, ); path = Common; sourceTree = ""; }; 4CE145D720EC0D5A000E01C9 /* Debug */ = { isa = PBXGroup; children = ( 4CE145DF20EC0D5A000E01C9 /* Enabled Features.xcconfig */, 4CE145DA20EC0D5A000E01C9 /* Textual.xcconfig */, 4CE145DE20EC0D5A000E01C9 /* XPC Service - ICL Extensions.xcconfig */, ); path = Debug; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 4CA1FE571FA606890095901F /* Inline Content Loader Core Media */ = { isa = PBXNativeTarget; buildConfigurationList = 4CA1FE5E1FA606890095901F /* Build configuration list for PBXNativeTarget "Inline Content Loader Core Media" */; buildPhases = ( 4CA1FE561FA606890095901F /* Resources */, 4CA1FE551FA606890095901F /* Frameworks */, 4CA1FE541FA606890095901F /* Sources */, ); buildRules = ( ); dependencies = ( ); name = "Inline Content Loader Core Media"; productName = "Core Media"; productReference = 4CA1FE581FA606890095901F /* Core Media.mediaPlugin */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 4CA1FE501FA606890095901F /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; CLASSPREFIX = ICP; LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Codeux Software, LLC"; TargetAttributes = { 4CA1FE571FA606890095901F = { CreatedOnToolsVersion = 9.0; }; }; }; buildConfigurationList = 4CA1FE531FA606890095901F /* Build configuration list for PBXProject "Inline Content Loader Core Media" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 4CA1FE4F1FA606890095901F; productRefGroup = 4CA1FE591FA606890095901F /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4CA1FE571FA606890095901F /* Inline Content Loader Core Media */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4CA1FE561FA606890095901F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C469F3920EC608E00094EA4 /* ICMVimeo.mustache in Resources */, 4C469F3E20EC608E00094EA4 /* ICMTwitchLive.mustache in Resources */, 4C469F3B20EC608E00094EA4 /* ICMYouTube.mustache in Resources */, 4C469F3A20EC608E00094EA4 /* ICMPornhub.mustache in Resources */, 4C469F3720EC608E00094EA4 /* ICMTweet.js in Resources */, 4C469F3D20EC608E00094EA4 /* ICMTwitchClips.mustache in Resources */, 4C469F3C20EC608E00094EA4 /* ICMDailymotion.mustache in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 4CA1FE541FA606890095901F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C469ED720EC600900094EA4 /* ICMTweet.m in Sources */, 4C469EDF20EC600900094EA4 /* ICMYouTube.m in Sources */, 4C469EDA20EC600900094EA4 /* ICMGyazo.m in Sources */, 4C469EE220EC600900094EA4 /* ICMTwitchLive.m in Sources */, 4C469EE320EC600900094EA4 /* ICMCommonInlineImages.m in Sources */, 4C469EDB20EC600900094EA4 /* ICMStreamable.m in Sources */, 4C469EE420EC600900094EA4 /* ICMXkcd.m in Sources */, 4C469EE020EC600900094EA4 /* ICMDailymotion.m in Sources */, 4C469EE120EC600900094EA4 /* ICMTwitchClips.m in Sources */, 4C469ED520EC600900094EA4 /* ICPCoreMedia.m in Sources */, 4C469EDC20EC600900094EA4 /* ICMVimeo.m in Sources */, 4C469EDD20EC600900094EA4 /* ICMPornhub.m in Sources */, 4C469EDE20EC600900094EA4 /* ICMImgurGifv.m in Sources */, 4C469ED620EC600900094EA4 /* ICMCommonInlineVideos.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 4CA1FE5C1FA606890095901F /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE145DE20EC0D5A000E01C9 /* XPC Service - ICL Extensions.xcconfig */; buildSettings = { }; name = Debug; }; 4CA1FE5D1FA606890095901F /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE145B620EC0D5A000E01C9 /* XPC Service - ICL Extensions.xcconfig */; buildSettings = { }; name = Release; }; 4CA1FE5F1FA606890095901F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { GCC_PREFIX_HEADER = "${PROJECT_DIR}/Classes/Headers/Private/ICPCoreMediaPCHPrivate.h"; INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.InlineContentLoader-CoreMedia"; PRODUCT_NAME = "Core Media"; WRAPPER_EXTENSION = mediaPlugin; }; name = Debug; }; 4CA1FE601FA606890095901F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { GCC_PREFIX_HEADER = "${PROJECT_DIR}/Classes/Headers/Private/ICPCoreMediaPCHPrivate.h"; INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-extensions.InlineContentLoader-CoreMedia"; PRODUCT_NAME = "Core Media"; WRAPPER_EXTENSION = mediaPlugin; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 4CA1FE531FA606890095901F /* Build configuration list for PBXProject "Inline Content Loader Core Media" */ = { isa = XCConfigurationList; buildConfigurations = ( 4CA1FE5C1FA606890095901F /* Debug */, 4CA1FE5D1FA606890095901F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4CA1FE5E1FA606890095901F /* Build configuration list for PBXNativeTarget "Inline Content Loader Core Media" */ = { isa = XCConfigurationList; buildConfigurations = ( 4CA1FE5F1FA606890095901F /* Debug */, 4CA1FE601FA606890095901F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 4CA1FE501FA606890095901F /* Project object */; } ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Resources/Modules/Dailymotion/ICMDailymotion.mustache ================================================ × ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Resources/Modules/Pornhub/ICMPornhub.mustache ================================================ × ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Resources/Modules/Twitch Clips/ICMTwitchClips.mustache ================================================ × ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Resources/Modules/Twitch Live/ICMTwitchLive.mustache ================================================ × ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Resources/Modules/Twitter/ICMTweet.js ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ window.twttr = (function() { /* cancer */ var t = window.twttr; if (t) { return t; } t = {}; t._e = []; t.ready = function(f) { t._e.push(f); }; return t; }()); var _ICMTweetPrototypeParent = _ICMInlineHTMLPrototype; var _ICMTweetPrototype = function() { _ICMTweetPrototypeParent.call(this); } _ICMTweetPrototype.prototype = Object.create(_ICMInlineHTMLPrototype.prototype); _ICMTweetPrototype.prototype.constructor = _ICMTweetPrototype; _ICMTweetPrototype.prototype.superClass = _ICMTweetPrototypeParent.prototype; _ICMTweetPrototype.prototype.didLoadMedia = function(mediaId, mediaElement) { /* We do not have enough context in the module itself to set the appearance of it, but we do once we reach the entrypoint. */ var themeAppearance = document.body.dataset.appearance; if (themeAppearance === "dark") { var tweet = mediaElement.querySelector("blockquote.twitter-tweet"); tweet.dataset.theme = "dark"; } twttr.widgets.load(mediaElement); }; _ICMTweetPrototype.prototype.tweetRendered = function(event) { TextualScroller.restoreScrolledToBottom(); }; var _ICMTweet = null; twttr.ready( function(twttr) { _ICMTweet = new _ICMTweetPrototype(); twttr.events.bind("rendered", _ICMTweet.tweetRendered); } ); ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Resources/Modules/Vimeo/ICMVimeo.mustache ================================================ × ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Resources/Modules/YouTube/ICMYouTube.mustache ================================================ × ================================================ FILE: XPC Services/Inline Content Loader/Extensions/Core Media/Resources/Property Lists/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleVersion 1.0.0 NSHumanReadableCopyright Copyright © 2017, 2018 Codeux Software, LLC. All rights reserved. NSPrincipalClass ICPCoreMedia ================================================ FILE: XPC Services/Inline Content Loader/Inline Content Loader.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 4C469F6C20EC60F000094EA4 /* ICLHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4220EC60F000094EA4 /* ICLHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F6D20EC60F000094EA4 /* ICLPayloadInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4420EC60F000094EA4 /* ICLPayloadInternal.h */; }; 4C469F6E20EC60F100094EA4 /* ICLInlineContentModuleInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4520EC60F000094EA4 /* ICLInlineContentModuleInternal.h */; }; 4C469F6F20EC60F100094EA4 /* ICLMediaAssessmentInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4620EC60F000094EA4 /* ICLMediaAssessmentInternal.h */; }; 4C469F7020EC60F100094EA4 /* ICLMediaType.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4720EC60F000094EA4 /* ICLMediaType.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F7120EC60F100094EA4 /* ICLMediaAssessment.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4820EC60F000094EA4 /* ICLMediaAssessment.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F7220EC60F100094EA4 /* ICLPayload.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4920EC60F000094EA4 /* ICLPayload.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F7320EC60F100094EA4 /* ICLPluginProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4A20EC60F000094EA4 /* ICLPluginProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F7420EC60F100094EA4 /* ICLProcessDelegatePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4C20EC60F000094EA4 /* ICLProcessDelegatePrivate.h */; }; 4C469F7520EC60F100094EA4 /* ICLPayloadPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4D20EC60F000094EA4 /* ICLPayloadPrivate.h */; }; 4C469F7620EC60F100094EA4 /* ICLProcessMainPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4E20EC60F000094EA4 /* ICLProcessMainPrivate.h */; }; 4C469F7720EC60F100094EA4 /* ICLProcessPCHPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F4F20EC60F000094EA4 /* ICLProcessPCHPrivate.h */; }; 4C469F7820EC60F100094EA4 /* ICLInlineContentProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F5020EC60F000094EA4 /* ICLInlineContentProtocol.h */; }; 4C469F7920EC60F100094EA4 /* ICLPluginManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F5120EC60F000094EA4 /* ICLPluginManagerPrivate.h */; }; 4C469F7A20EC60F100094EA4 /* ICLInlineContentModulePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F5220EC60F000094EA4 /* ICLInlineContentModulePrivate.h */; }; 4C469F7B20EC60F100094EA4 /* CoreModulesImportsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F5320EC60F000094EA4 /* CoreModulesImportsPrivate.h */; }; 4C469F7C20EC60F100094EA4 /* ICLMediaAssessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F5420EC60F000094EA4 /* ICLMediaAssessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F7D20EC60F100094EA4 /* ICLInlineContentModule.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F5520EC60F000094EA4 /* ICLInlineContentModule.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F7E20EC60F100094EA4 /* ICLInlineContentModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5720EC60F000094EA4 /* ICLInlineContentModule.m */; }; 4C469F7F20EC60F100094EA4 /* ICLMediaAssessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5820EC60F000094EA4 /* ICLMediaAssessor.m */; }; 4C469F8020EC60F100094EA4 /* ICLPayloadLocal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5920EC60F000094EA4 /* ICLPayloadLocal.m */; }; 4C469F8120EC60F100094EA4 /* ICLProcessMain.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5A20EC60F000094EA4 /* ICLProcessMain.m */; }; 4C469F8220EC60F100094EA4 /* ICLProcessDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5B20EC60F000094EA4 /* ICLProcessDelegate.m */; }; 4C469F8320EC60F100094EA4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5C20EC60F000094EA4 /* main.m */; }; 4C469F8420EC60F100094EA4 /* ICLHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5D20EC60F000094EA4 /* ICLHelpers.m */; }; 4C469F8520EC60F100094EA4 /* ICLPayloadShared.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5E20EC60F000094EA4 /* ICLPayloadShared.m */; }; 4C469F8620EC60F100094EA4 /* ICLPluginManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F5F20EC60F000094EA4 /* ICLPluginManager.m */; }; 4C469F8720EC60F100094EA4 /* ICLMediaAssessment.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F6020EC60F000094EA4 /* ICLMediaAssessment.m */; }; 4C469F8820EC60F100094EA4 /* ICMAssessedMedia.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F6320EC60F000094EA4 /* ICMAssessedMedia.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F8920EC60F100094EA4 /* ICMAssessedMedia.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F6420EC60F000094EA4 /* ICMAssessedMedia.m */; }; 4C469F8A20EC60F100094EA4 /* ICMInlineVideo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F6620EC60F000094EA4 /* ICMInlineVideo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F8B20EC60F100094EA4 /* ICMInlineHTML.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F6720EC60F000094EA4 /* ICMInlineHTML.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F8C20EC60F100094EA4 /* ICMInlineImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469F6820EC60F000094EA4 /* ICMInlineImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4C469F8D20EC60F100094EA4 /* ICMInlineVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F6920EC60F000094EA4 /* ICMInlineVideo.m */; }; 4C469F8E20EC60F100094EA4 /* ICMInlineImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F6A20EC60F000094EA4 /* ICMInlineImage.m */; }; 4C469F8F20EC60F100094EA4 /* ICMInlineHTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F6B20EC60F000094EA4 /* ICMInlineHTML.m */; }; 4C469FC220EC610000094EA4 /* TPCPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F9220EC610000094EA4 /* TPCPreferences.m */; }; 4C469FC320EC610000094EA4 /* TPCPreferencesUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C469F9320EC610000094EA4 /* TPCPreferencesUserDefaults.m */; }; 4C469FCF20EC610000094EA4 /* TPCPreferencesUserDefaults.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469FA520EC610000094EA4 /* TPCPreferencesUserDefaults.h */; }; 4C469FD520EC610000094EA4 /* TPCPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C469FAC20EC610000094EA4 /* TPCPreferences.h */; }; 4C469FF820EC61C900094EA4 /* InlineImageLiveResize.js in Resources */ = {isa = PBXBuildFile; fileRef = 4C469FEC20EC61C900094EA4 /* InlineImageLiveResize.js */; }; 4C46A01220EC666400094EA4 /* ICMInlineImage.css in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FE420EC61C900094EA4 /* ICMInlineImage.css */; }; 4C46A01320EC666400094EA4 /* ICMInlineImage.js in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FE520EC61C900094EA4 /* ICMInlineImage.js */; }; 4C46A01420EC666400094EA4 /* ICMInlineVideo.js in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FE620EC61C900094EA4 /* ICMInlineVideo.js */; }; 4C46A01520EC666400094EA4 /* ICMInlineHTML.mustache in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FE720EC61C900094EA4 /* ICMInlineHTML.mustache */; }; 4C46A01620EC666400094EA4 /* ICMInlineHTML.css in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FE820EC61C900094EA4 /* ICMInlineHTML.css */; }; 4C46A01720EC666400094EA4 /* ICMInlineVideo.css in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FE920EC61C900094EA4 /* ICMInlineVideo.css */; }; 4C46A01820EC666400094EA4 /* ICMInlineImage.mustache in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FEA20EC61C900094EA4 /* ICMInlineImage.mustache */; }; 4C46A01920EC666400094EA4 /* ICMInlineHTML.js in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FEB20EC61C900094EA4 /* ICMInlineHTML.js */; }; 4C46A01A20EC666400094EA4 /* InlineImageLiveResize.js in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FEC20EC61C900094EA4 /* InlineImageLiveResize.js */; }; 4C46A01B20EC666400094EA4 /* ICMInlineVideo.mustache in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 4C469FED20EC61C900094EA4 /* ICMInlineVideo.mustache */; }; 4C8C84391F8E5AEC00062FFD /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8C84381F8E5AE400062FFD /* ImageIO.framework */; }; 4C9A933A1FA65970003C3DB5 /* Core Media.mediaPlugin in Copy Extensions */ = {isa = PBXBuildFile; fileRef = 4C9A93391FA65970003C3DB5 /* Core Media.mediaPlugin */; }; 4CE1448020EC01AD000E01C9 /* CocoaExtensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CE1447F20EC01AD000E01C9 /* CocoaExtensions.framework */; }; 4CE58BAA1F8A5FB800493E7C /* GRMustache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CE58BA91F8A5F8500493E7C /* GRMustache.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 4C03B6781FA656CE00748FE7 /* Copy Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Extensions; dstSubfolderSpec = 7; files = ( 4C9A933A1FA65970003C3DB5 /* Core Media.mediaPlugin in Copy Extensions */, ); name = "Copy Extensions"; runOnlyForDeploymentPostprocessing = 0; }; 4C8C842B1F8E347500062FFD /* Copy Bundle Resources */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Components; dstSubfolderSpec = 7; files = ( 4C46A01220EC666400094EA4 /* ICMInlineImage.css in Copy Bundle Resources */, 4C46A01320EC666400094EA4 /* ICMInlineImage.js in Copy Bundle Resources */, 4C46A01420EC666400094EA4 /* ICMInlineVideo.js in Copy Bundle Resources */, 4C46A01520EC666400094EA4 /* ICMInlineHTML.mustache in Copy Bundle Resources */, 4C46A01620EC666400094EA4 /* ICMInlineHTML.css in Copy Bundle Resources */, 4C46A01720EC666400094EA4 /* ICMInlineVideo.css in Copy Bundle Resources */, 4C46A01820EC666400094EA4 /* ICMInlineImage.mustache in Copy Bundle Resources */, 4C46A01920EC666400094EA4 /* ICMInlineHTML.js in Copy Bundle Resources */, 4C46A01A20EC666400094EA4 /* InlineImageLiveResize.js in Copy Bundle Resources */, 4C46A01B20EC666400094EA4 /* ICMInlineVideo.mustache in Copy Bundle Resources */, ); name = "Copy Bundle Resources"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 4C1406421F11942B00C73D53 /* Inline Content Loader.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = "Inline Content Loader.xpc"; sourceTree = BUILT_PRODUCTS_DIR; }; 4C1EAB161F89F2BF00A4E647 /* BuildConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BuildConfig.h; path = ../../.tmp/BuildConfig.h; sourceTree = SOURCE_ROOT; }; 4C469F4220EC60F000094EA4 /* ICLHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLHelpers.h; sourceTree = ""; }; 4C469F4420EC60F000094EA4 /* ICLPayloadInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLPayloadInternal.h; sourceTree = ""; }; 4C469F4520EC60F000094EA4 /* ICLInlineContentModuleInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLInlineContentModuleInternal.h; sourceTree = ""; }; 4C469F4620EC60F000094EA4 /* ICLMediaAssessmentInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLMediaAssessmentInternal.h; sourceTree = ""; }; 4C469F4720EC60F000094EA4 /* ICLMediaType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLMediaType.h; sourceTree = ""; }; 4C469F4820EC60F000094EA4 /* ICLMediaAssessment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLMediaAssessment.h; sourceTree = ""; }; 4C469F4920EC60F000094EA4 /* ICLPayload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLPayload.h; sourceTree = ""; }; 4C469F4A20EC60F000094EA4 /* ICLPluginProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLPluginProtocol.h; sourceTree = ""; }; 4C469F4C20EC60F000094EA4 /* ICLProcessDelegatePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLProcessDelegatePrivate.h; sourceTree = ""; }; 4C469F4D20EC60F000094EA4 /* ICLPayloadPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLPayloadPrivate.h; sourceTree = ""; }; 4C469F4E20EC60F000094EA4 /* ICLProcessMainPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLProcessMainPrivate.h; sourceTree = ""; }; 4C469F4F20EC60F000094EA4 /* ICLProcessPCHPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLProcessPCHPrivate.h; sourceTree = ""; }; 4C469F5020EC60F000094EA4 /* ICLInlineContentProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLInlineContentProtocol.h; sourceTree = ""; }; 4C469F5120EC60F000094EA4 /* ICLPluginManagerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLPluginManagerPrivate.h; sourceTree = ""; }; 4C469F5220EC60F000094EA4 /* ICLInlineContentModulePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLInlineContentModulePrivate.h; sourceTree = ""; }; 4C469F5320EC60F000094EA4 /* CoreModulesImportsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoreModulesImportsPrivate.h; sourceTree = ""; }; 4C469F5420EC60F000094EA4 /* ICLMediaAssessor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLMediaAssessor.h; sourceTree = ""; }; 4C469F5520EC60F000094EA4 /* ICLInlineContentModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICLInlineContentModule.h; sourceTree = ""; }; 4C469F5720EC60F000094EA4 /* ICLInlineContentModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLInlineContentModule.m; sourceTree = ""; }; 4C469F5820EC60F000094EA4 /* ICLMediaAssessor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLMediaAssessor.m; sourceTree = ""; }; 4C469F5920EC60F000094EA4 /* ICLPayloadLocal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLPayloadLocal.m; sourceTree = ""; }; 4C469F5A20EC60F000094EA4 /* ICLProcessMain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLProcessMain.m; sourceTree = ""; }; 4C469F5B20EC60F000094EA4 /* ICLProcessDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLProcessDelegate.m; sourceTree = ""; }; 4C469F5C20EC60F000094EA4 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4C469F5D20EC60F000094EA4 /* ICLHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLHelpers.m; sourceTree = ""; }; 4C469F5E20EC60F000094EA4 /* ICLPayloadShared.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLPayloadShared.m; sourceTree = ""; }; 4C469F5F20EC60F000094EA4 /* ICLPluginManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLPluginManager.m; sourceTree = ""; }; 4C469F6020EC60F000094EA4 /* ICLMediaAssessment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICLMediaAssessment.m; sourceTree = ""; }; 4C469F6320EC60F000094EA4 /* ICMAssessedMedia.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMAssessedMedia.h; sourceTree = ""; }; 4C469F6420EC60F000094EA4 /* ICMAssessedMedia.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMAssessedMedia.m; sourceTree = ""; }; 4C469F6620EC60F000094EA4 /* ICMInlineVideo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMInlineVideo.h; sourceTree = ""; }; 4C469F6720EC60F000094EA4 /* ICMInlineHTML.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMInlineHTML.h; sourceTree = ""; }; 4C469F6820EC60F000094EA4 /* ICMInlineImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ICMInlineImage.h; sourceTree = ""; }; 4C469F6920EC60F000094EA4 /* ICMInlineVideo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMInlineVideo.m; sourceTree = ""; }; 4C469F6A20EC60F000094EA4 /* ICMInlineImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMInlineImage.m; sourceTree = ""; }; 4C469F6B20EC60F000094EA4 /* ICMInlineHTML.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ICMInlineHTML.m; sourceTree = ""; }; 4C469F9220EC610000094EA4 /* TPCPreferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPCPreferences.m; sourceTree = ""; }; 4C469F9320EC610000094EA4 /* TPCPreferencesUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TPCPreferencesUserDefaults.m; sourceTree = ""; }; 4C469FA520EC610000094EA4 /* TPCPreferencesUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPCPreferencesUserDefaults.h; sourceTree = ""; }; 4C469FAC20EC610000094EA4 /* TPCPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TPCPreferences.h; sourceTree = ""; }; 4C469FE420EC61C900094EA4 /* ICMInlineImage.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = ICMInlineImage.css; sourceTree = ""; }; 4C469FE520EC61C900094EA4 /* ICMInlineImage.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ICMInlineImage.js; sourceTree = ""; }; 4C469FE620EC61C900094EA4 /* ICMInlineVideo.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ICMInlineVideo.js; sourceTree = ""; }; 4C469FE720EC61C900094EA4 /* ICMInlineHTML.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMInlineHTML.mustache; sourceTree = ""; }; 4C469FE820EC61C900094EA4 /* ICMInlineHTML.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = ICMInlineHTML.css; sourceTree = ""; }; 4C469FE920EC61C900094EA4 /* ICMInlineVideo.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = ICMInlineVideo.css; sourceTree = ""; }; 4C469FEA20EC61C900094EA4 /* ICMInlineImage.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMInlineImage.mustache; sourceTree = ""; }; 4C469FEB20EC61C900094EA4 /* ICMInlineHTML.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ICMInlineHTML.js; sourceTree = ""; }; 4C469FEC20EC61C900094EA4 /* InlineImageLiveResize.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = InlineImageLiveResize.js; sourceTree = ""; }; 4C469FED20EC61C900094EA4 /* ICMInlineVideo.mustache */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ICMInlineVideo.mustache; sourceTree = ""; }; 4C469FEF20EC61C900094EA4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4C8C84381F8E5AE400062FFD /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; 4C9A93391FA65970003C3DB5 /* Core Media.mediaPlugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "Core Media.mediaPlugin"; path = "../../../.tmp/SharedBuildProducts-ICLExtension/Core Media.mediaPlugin"; sourceTree = ""; }; 4CA1FE8D1FA6261A0095901F /* BuildExtensions.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = BuildExtensions.sh; sourceTree = ""; }; 4CE1447F20EC01AD000E01C9 /* CocoaExtensions.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaExtensions.framework; path = "../../.tmp/SharedBuildProducts-Frameworks/CocoaExtensions.framework"; sourceTree = SOURCE_ROOT; }; 4CE1448720EC02A3000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1448920EC02A3000E01C9 /* XPC Service - ICL.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL.xcconfig"; sourceTree = ""; }; 4CE1448A20EC02A3000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE1448C20EC02A3000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE1449A20EC02A3000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE1449B20EC02A3000E01C9 /* Preserve Symbols.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Preserve Symbols.xcconfig"; sourceTree = ""; }; 4CE1449D20EC02A3000E01C9 /* XPC Service - ICL.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL.xcconfig"; sourceTree = ""; }; 4CE1449E20EC02A3000E01C9 /* Foundation.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Foundation.xcconfig; sourceTree = ""; }; 4CE1449F20EC02A3000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE144A120EC02A3000E01C9 /* Foundation Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Foundation Debug.xcconfig"; sourceTree = ""; }; 4CE144AF20EC02A3000E01C9 /* Textual.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Textual.xcconfig; sourceTree = ""; }; 4CE144B120EC02A3000E01C9 /* XPC Service - ICL.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Service - ICL.xcconfig"; sourceTree = ""; }; 4CE144B220EC02A3000E01C9 /* XPC Services.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "XPC Services.xcconfig"; sourceTree = ""; }; 4CE144B420EC02A3000E01C9 /* Enabled Features.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Enabled Features.xcconfig"; sourceTree = ""; }; 4CE144E420EC02BD000E01C9 /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 4CE144E520EC0381000E01C9 /* ICLInlineContentProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ICLInlineContentProtocol.h; path = Classes/Headers/Private/ICLInlineContentProtocol.h; sourceTree = SOURCE_ROOT; }; 4CE58BA91F8A5F8500493E7C /* GRMustache.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GRMustache.framework; path = "../../Frameworks/Static Libraries/Libraries/GRMustache.framework"; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 4C14063F1F11942B00C73D53 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4CE58BAA1F8A5FB800493E7C /* GRMustache.framework in Frameworks */, 4C8C84391F8E5AEC00062FFD /* ImageIO.framework in Frameworks */, 4CE1448020EC01AD000E01C9 /* CocoaExtensions.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4C1406391F11942B00C73D53 = { isa = PBXGroup; children = ( 4C469F4020EC60F000094EA4 /* Classes */, 4C1406521F1194E900C73D53 /* Resources */, 4C8C72D71F1208F200ED72DB /* Frameworks */, 4C1406431F11942B00C73D53 /* Products */, ); sourceTree = ""; }; 4C1406431F11942B00C73D53 /* Products */ = { isa = PBXGroup; children = ( 4C1406421F11942B00C73D53 /* Inline Content Loader.xpc */, ); name = Products; sourceTree = ""; }; 4C1406521F1194E900C73D53 /* Resources */ = { isa = PBXGroup; children = ( 4C1406531F1194F100C73D53 /* Build Settings */, 4C9A93381FA65960003C3DB5 /* Extensions */, 4C469FE320EC61C900094EA4 /* Modules */, 4C469FEE20EC61C900094EA4 /* Property Lists */, ); path = Resources; sourceTree = ""; }; 4C1406531F1194F100C73D53 /* Build Settings */ = { isa = PBXGroup; children = ( 4CA1FE8E1FA6261D0095901F /* Build Scripts */, 4C1406541F1194F900C73D53 /* Configurations */, 4C1EAB161F89F2BF00A4E647 /* BuildConfig.h */, ); name = "Build Settings"; sourceTree = ""; }; 4C1406541F1194F900C73D53 /* Configurations */ = { isa = PBXGroup; children = ( 4CE1448320EC02A3000E01C9 /* Build */, 4CE144B520EC02A3000E01C9 /* Sandbox */, ); name = Configurations; path = ../../Configurations; sourceTree = SOURCE_ROOT; }; 4C469F4020EC60F000094EA4 /* Classes */ = { isa = PBXGroup; children = ( 4C469F4120EC60F000094EA4 /* Headers */, 4C469F6120EC60F000094EA4 /* Modules */, 4C469F5620EC60F000094EA4 /* Service */, 4C469F9020EC610000094EA4 /* Shared */, ); path = Classes; sourceTree = ""; }; 4C469F4120EC60F000094EA4 /* Headers */ = { isa = PBXGroup; children = ( 4C469F4320EC60F000094EA4 /* Internal */, 4C469F4B20EC60F000094EA4 /* Private */, 4C469F4220EC60F000094EA4 /* ICLHelpers.h */, 4C469F5520EC60F000094EA4 /* ICLInlineContentModule.h */, 4C469F4820EC60F000094EA4 /* ICLMediaAssessment.h */, 4C469F5420EC60F000094EA4 /* ICLMediaAssessor.h */, 4C469F4720EC60F000094EA4 /* ICLMediaType.h */, 4C469F4920EC60F000094EA4 /* ICLPayload.h */, 4C469F4A20EC60F000094EA4 /* ICLPluginProtocol.h */, ); path = Headers; sourceTree = ""; }; 4C469F4320EC60F000094EA4 /* Internal */ = { isa = PBXGroup; children = ( 4C469F4420EC60F000094EA4 /* ICLPayloadInternal.h */, 4C469F4520EC60F000094EA4 /* ICLInlineContentModuleInternal.h */, 4C469F4620EC60F000094EA4 /* ICLMediaAssessmentInternal.h */, ); path = Internal; sourceTree = ""; }; 4C469F4B20EC60F000094EA4 /* Private */ = { isa = PBXGroup; children = ( 4C469F4C20EC60F000094EA4 /* ICLProcessDelegatePrivate.h */, 4C469F4D20EC60F000094EA4 /* ICLPayloadPrivate.h */, 4C469F4E20EC60F000094EA4 /* ICLProcessMainPrivate.h */, 4C469F4F20EC60F000094EA4 /* ICLProcessPCHPrivate.h */, 4C469F5020EC60F000094EA4 /* ICLInlineContentProtocol.h */, 4C469F5120EC60F000094EA4 /* ICLPluginManagerPrivate.h */, 4C469F5220EC60F000094EA4 /* ICLInlineContentModulePrivate.h */, 4C469F5320EC60F000094EA4 /* CoreModulesImportsPrivate.h */, ); path = Private; sourceTree = ""; }; 4C469F5620EC60F000094EA4 /* Service */ = { isa = PBXGroup; children = ( 4C469F5D20EC60F000094EA4 /* ICLHelpers.m */, 4C469F5720EC60F000094EA4 /* ICLInlineContentModule.m */, 4C469F6020EC60F000094EA4 /* ICLMediaAssessment.m */, 4C469F5820EC60F000094EA4 /* ICLMediaAssessor.m */, 4C469F5920EC60F000094EA4 /* ICLPayloadLocal.m */, 4C469F5E20EC60F000094EA4 /* ICLPayloadShared.m */, 4C469F5F20EC60F000094EA4 /* ICLPluginManager.m */, 4C469F5B20EC60F000094EA4 /* ICLProcessDelegate.m */, 4C469F5A20EC60F000094EA4 /* ICLProcessMain.m */, 4C469F5C20EC60F000094EA4 /* main.m */, ); path = Service; sourceTree = ""; }; 4C469F6120EC60F000094EA4 /* Modules */ = { isa = PBXGroup; children = ( 4C469F6220EC60F000094EA4 /* Assessed Media */, 4C469F6520EC60F000094EA4 /* Root Classes */, ); path = Modules; sourceTree = ""; }; 4C469F6220EC60F000094EA4 /* Assessed Media */ = { isa = PBXGroup; children = ( 4C469F6320EC60F000094EA4 /* ICMAssessedMedia.h */, 4C469F6420EC60F000094EA4 /* ICMAssessedMedia.m */, ); path = "Assessed Media"; sourceTree = ""; }; 4C469F6520EC60F000094EA4 /* Root Classes */ = { isa = PBXGroup; children = ( 4C469F6620EC60F000094EA4 /* ICMInlineVideo.h */, 4C469F6720EC60F000094EA4 /* ICMInlineHTML.h */, 4C469F6820EC60F000094EA4 /* ICMInlineImage.h */, 4C469F6920EC60F000094EA4 /* ICMInlineVideo.m */, 4C469F6A20EC60F000094EA4 /* ICMInlineImage.m */, 4C469F6B20EC60F000094EA4 /* ICMInlineHTML.m */, ); path = "Root Classes"; sourceTree = ""; }; 4C469F9020EC610000094EA4 /* Shared */ = { isa = PBXGroup; children = ( 4C469F9120EC610000094EA4 /* Preferences */, 4C469F9F20EC610000094EA4 /* Headers */, ); name = Shared; path = ../../Sources/Shared; sourceTree = SOURCE_ROOT; }; 4C469F9120EC610000094EA4 /* Preferences */ = { isa = PBXGroup; children = ( 4C469F9220EC610000094EA4 /* TPCPreferences.m */, 4C469F9320EC610000094EA4 /* TPCPreferencesUserDefaults.m */, ); path = Preferences; sourceTree = ""; }; 4C469F9F20EC610000094EA4 /* Headers */ = { isa = PBXGroup; children = ( 4C469FB420EC610000094EA4 /* Services */, 4C469FAC20EC610000094EA4 /* TPCPreferences.h */, 4C469FA520EC610000094EA4 /* TPCPreferencesUserDefaults.h */, ); path = Headers; sourceTree = ""; }; 4C469FB420EC610000094EA4 /* Services */ = { isa = PBXGroup; children = ( 4CE144E520EC0381000E01C9 /* ICLInlineContentProtocol.h */, ); path = Services; sourceTree = ""; }; 4C469FE320EC61C900094EA4 /* Modules */ = { isa = PBXGroup; children = ( 4C469FE420EC61C900094EA4 /* ICMInlineImage.css */, 4C469FE520EC61C900094EA4 /* ICMInlineImage.js */, 4C469FE620EC61C900094EA4 /* ICMInlineVideo.js */, 4C469FE720EC61C900094EA4 /* ICMInlineHTML.mustache */, 4C469FE820EC61C900094EA4 /* ICMInlineHTML.css */, 4C469FE920EC61C900094EA4 /* ICMInlineVideo.css */, 4C469FEA20EC61C900094EA4 /* ICMInlineImage.mustache */, 4C469FEB20EC61C900094EA4 /* ICMInlineHTML.js */, 4C469FEC20EC61C900094EA4 /* InlineImageLiveResize.js */, 4C469FED20EC61C900094EA4 /* ICMInlineVideo.mustache */, ); path = Modules; sourceTree = ""; }; 4C469FEE20EC61C900094EA4 /* Property Lists */ = { isa = PBXGroup; children = ( 4C469FEF20EC61C900094EA4 /* Info.plist */, ); path = "Property Lists"; sourceTree = ""; }; 4C8C72D31F1208F200ED72DB /* External Frameworks */ = { isa = PBXGroup; children = ( 4CE58BA91F8A5F8500493E7C /* GRMustache.framework */, 4CE1447F20EC01AD000E01C9 /* CocoaExtensions.framework */, ); name = "External Frameworks"; sourceTree = ""; }; 4C8C72D61F1208F200ED72DB /* System Frameworks */ = { isa = PBXGroup; children = ( 4C8C84381F8E5AE400062FFD /* ImageIO.framework */, ); name = "System Frameworks"; sourceTree = ""; }; 4C8C72D71F1208F200ED72DB /* Frameworks */ = { isa = PBXGroup; children = ( 4C8C72D31F1208F200ED72DB /* External Frameworks */, 4C8C72D61F1208F200ED72DB /* System Frameworks */, ); name = Frameworks; sourceTree = ""; }; 4C9A93381FA65960003C3DB5 /* Extensions */ = { isa = PBXGroup; children = ( 4C9A93391FA65970003C3DB5 /* Core Media.mediaPlugin */, ); name = Extensions; sourceTree = ""; }; 4CA1FE8E1FA6261D0095901F /* Build Scripts */ = { isa = PBXGroup; children = ( 4CA1FE8D1FA6261A0095901F /* BuildExtensions.sh */, ); path = "Build Scripts"; sourceTree = SOURCE_ROOT; }; 4CE1448320EC02A3000E01C9 /* Build */ = { isa = PBXGroup; children = ( 4CE1449720EC02A3000E01C9 /* Common */, 4CE144AC20EC02A3000E01C9 /* Debug */, 4CE1448420EC02A3000E01C9 /* Standard Release */, ); path = Build; sourceTree = ""; }; 4CE1448420EC02A3000E01C9 /* Standard Release */ = { isa = PBXGroup; children = ( 4CE1448C20EC02A3000E01C9 /* Enabled Features.xcconfig */, 4CE1448720EC02A3000E01C9 /* Textual.xcconfig */, 4CE1448920EC02A3000E01C9 /* XPC Service - ICL.xcconfig */, 4CE1448A20EC02A3000E01C9 /* XPC Services.xcconfig */, ); path = "Standard Release"; sourceTree = ""; }; 4CE1449720EC02A3000E01C9 /* Common */ = { isa = PBXGroup; children = ( 4CE144A120EC02A3000E01C9 /* Foundation Debug.xcconfig */, 4CE1449E20EC02A3000E01C9 /* Foundation.xcconfig */, 4CE1449B20EC02A3000E01C9 /* Preserve Symbols.xcconfig */, 4CE1449A20EC02A3000E01C9 /* Textual.xcconfig */, 4CE1449D20EC02A3000E01C9 /* XPC Service - ICL.xcconfig */, 4CE1449F20EC02A3000E01C9 /* XPC Services.xcconfig */, ); path = Common; sourceTree = ""; }; 4CE144AC20EC02A3000E01C9 /* Debug */ = { isa = PBXGroup; children = ( 4CE144B420EC02A3000E01C9 /* Enabled Features.xcconfig */, 4CE144AF20EC02A3000E01C9 /* Textual.xcconfig */, 4CE144B120EC02A3000E01C9 /* XPC Service - ICL.xcconfig */, 4CE144B220EC02A3000E01C9 /* XPC Services.xcconfig */, ); path = Debug; sourceTree = ""; }; 4CE144B520EC02A3000E01C9 /* Sandbox */ = { isa = PBXGroup; children = ( 4CE144E420EC02BD000E01C9 /* Release.entitlements */, ); name = Sandbox; path = Configurations/Sandbox; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 4CB5C5411FA225EA00E65FB5 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 4C469F8C20EC60F100094EA4 /* ICMInlineImage.h in Headers */, 4C469FCF20EC610000094EA4 /* TPCPreferencesUserDefaults.h in Headers */, 4C469F6C20EC60F000094EA4 /* ICLHelpers.h in Headers */, 4C469FD520EC610000094EA4 /* TPCPreferences.h in Headers */, 4C469F7620EC60F100094EA4 /* ICLProcessMainPrivate.h in Headers */, 4C469F7420EC60F100094EA4 /* ICLProcessDelegatePrivate.h in Headers */, 4C469F7D20EC60F100094EA4 /* ICLInlineContentModule.h in Headers */, 4C469F7820EC60F100094EA4 /* ICLInlineContentProtocol.h in Headers */, 4C469F6E20EC60F100094EA4 /* ICLInlineContentModuleInternal.h in Headers */, 4C469F6F20EC60F100094EA4 /* ICLMediaAssessmentInternal.h in Headers */, 4C469F8A20EC60F100094EA4 /* ICMInlineVideo.h in Headers */, 4C469F7920EC60F100094EA4 /* ICLPluginManagerPrivate.h in Headers */, 4C469F7320EC60F100094EA4 /* ICLPluginProtocol.h in Headers */, 4C469F7120EC60F100094EA4 /* ICLMediaAssessment.h in Headers */, 4C469F6D20EC60F000094EA4 /* ICLPayloadInternal.h in Headers */, 4C469F7720EC60F100094EA4 /* ICLProcessPCHPrivate.h in Headers */, 4C469F7C20EC60F100094EA4 /* ICLMediaAssessor.h in Headers */, 4C469F7A20EC60F100094EA4 /* ICLInlineContentModulePrivate.h in Headers */, 4C469F8820EC60F100094EA4 /* ICMAssessedMedia.h in Headers */, 4C469F7020EC60F100094EA4 /* ICLMediaType.h in Headers */, 4C469F7B20EC60F100094EA4 /* CoreModulesImportsPrivate.h in Headers */, 4C469F7220EC60F100094EA4 /* ICLPayload.h in Headers */, 4C469F7520EC60F100094EA4 /* ICLPayloadPrivate.h in Headers */, 4C469F8B20EC60F100094EA4 /* ICMInlineHTML.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 4C1406411F11942B00C73D53 /* Inline Content Loader */ = { isa = PBXNativeTarget; buildConfigurationList = 4C14064E1F11942B00C73D53 /* Build configuration list for PBXNativeTarget "Inline Content Loader" */; buildPhases = ( 4C1406401F11942B00C73D53 /* Resources */, 4C8C842B1F8E347500062FFD /* Copy Bundle Resources */, 4C14063F1F11942B00C73D53 /* Frameworks */, 4C14063E1F11942B00C73D53 /* Sources */, 4CB5C5411FA225EA00E65FB5 /* Headers */, 4C03B5311FA6277A00748FE7 /* Build Extensions */, 4C03B6781FA656CE00748FE7 /* Copy Extensions */, ); buildRules = ( ); dependencies = ( ); name = "Inline Content Loader"; productName = "Connection Manager"; productReference = 4C1406421F11942B00C73D53 /* Inline Content Loader.xpc */; productType = "com.apple.product-type.xpc-service"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 4C14063A1F11942B00C73D53 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Codeux Software, LLC"; TargetAttributes = { 4C1406411F11942B00C73D53 = { CreatedOnToolsVersion = 8.0; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 4C14063D1F11942B00C73D53 /* Build configuration list for PBXProject "Inline Content Loader" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 4C1406391F11942B00C73D53; productRefGroup = 4C1406431F11942B00C73D53 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4C1406411F11942B00C73D53 /* Inline Content Loader */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 4C1406401F11942B00C73D53 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C469FF820EC61C900094EA4 /* InlineImageLiveResize.js in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 4C03B5311FA6277A00748FE7 /* Build Extensions */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Build Extensions"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\nexec \"${PROJECT_DIR}/Build Scripts/BuildExtensions.sh\" | tee \"${TEXTUAL_WORKSPACE_TEMP_DIR}/Script-Logs/BuildExtensions (ICL).txt\"\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 4C14063E1F11942B00C73D53 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 4C469F7F20EC60F100094EA4 /* ICLMediaAssessor.m in Sources */, 4C469FC220EC610000094EA4 /* TPCPreferences.m in Sources */, 4C469FC320EC610000094EA4 /* TPCPreferencesUserDefaults.m in Sources */, 4C469F8520EC60F100094EA4 /* ICLPayloadShared.m in Sources */, 4C469F8420EC60F100094EA4 /* ICLHelpers.m in Sources */, 4C469F8D20EC60F100094EA4 /* ICMInlineVideo.m in Sources */, 4C469F7E20EC60F100094EA4 /* ICLInlineContentModule.m in Sources */, 4C469F8020EC60F100094EA4 /* ICLPayloadLocal.m in Sources */, 4C469F8720EC60F100094EA4 /* ICLMediaAssessment.m in Sources */, 4C469F8620EC60F100094EA4 /* ICLPluginManager.m in Sources */, 4C469F8220EC60F100094EA4 /* ICLProcessDelegate.m in Sources */, 4C469F8120EC60F100094EA4 /* ICLProcessMain.m in Sources */, 4C469F8920EC60F100094EA4 /* ICMAssessedMedia.m in Sources */, 4C469F8F20EC60F100094EA4 /* ICMInlineHTML.m in Sources */, 4C469F8E20EC60F100094EA4 /* ICMInlineImage.m in Sources */, 4C469F8320EC60F100094EA4 /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 4C14064C1F11942B00C73D53 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE144B120EC02A3000E01C9 /* XPC Service - ICL.xcconfig */; buildSettings = { }; name = Debug; }; 4C14064D1F11942B00C73D53 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4CE1448920EC02A3000E01C9 /* XPC Service - ICL.xcconfig */; buildSettings = { }; name = Release; }; 4C14064F1F11942B00C73D53 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "${PROJECT_DIR}/Configurations/Sandbox/Release.entitlements"; GCC_PREFIX_HEADER = "${PROJECT_DIR}/Classes/Headers/Private/ICLProcessPCHPrivate.h"; INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-utilities.Textual-InlineContentLoader"; PRODUCT_NAME = "Inline Content Loader"; }; name = Debug; }; 4C1406501F11942B00C73D53 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "${PROJECT_DIR}/Configurations/Sandbox/Release.entitlements"; GCC_PREFIX_HEADER = "${PROJECT_DIR}/Classes/Headers/Private/ICLProcessPCHPrivate.h"; INFOPLIST_FILE = "Resources/Property Lists/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "com.codeux.app-utilities.Textual-InlineContentLoader"; PRODUCT_NAME = "Inline Content Loader"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 4C14063D1F11942B00C73D53 /* Build configuration list for PBXProject "Inline Content Loader" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C14064C1F11942B00C73D53 /* Debug */, 4C14064D1F11942B00C73D53 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4C14064E1F11942B00C73D53 /* Build configuration list for PBXNativeTarget "Inline Content Loader" */ = { isa = XCConfigurationList; buildConfigurations = ( 4C14064F1F11942B00C73D53 /* Debug */, 4C1406501F11942B00C73D53 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 4C14063A1F11942B00C73D53 /* Project object */; } ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineHTML.css ================================================ .inlineHTML { overflow: auto; display: block; margin-top: 12px; margin-bottom: 12px; text-indent: 0; } ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineHTML.js ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ var _ICMInlineHTMLPrototypeParent = InlineMediaPrototype; var _ICMInlineHTMLPrototype = function() { _ICMInlineHTMLPrototypeParent.call(this); } _ICMInlineHTMLPrototype.prototype = Object.create(InlineMediaPrototype.prototype); _ICMInlineHTMLPrototype.prototype.constructor = _ICMInlineHTMLPrototype; _ICMInlineHTMLPrototype.prototype.superClass = _ICMInlineHTMLPrototypeParent.prototype; var _ICMInlineHTML = new _ICMInlineHTMLPrototype(); ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineHTML.mustache ================================================ × {{{unescapedHTML}}} ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineImage.css ================================================ .inlineImage { overflow: auto; display: block; margin-top: 12px; margin-bottom: 12px; } ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineImage.js ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ var _ICMInlineImagePrototypeParent = InlineMediaPrototype; var _ICMInlineImagePrototype = function() { _ICMInlineImagePrototypeParent.call(this); } _ICMInlineImagePrototype.prototype = Object.create(InlineMediaPrototype.prototype); _ICMInlineImagePrototype.prototype.constructor = _ICMInlineImagePrototype; _ICMInlineImagePrototype.prototype.superClass = _ICMInlineImagePrototypeParent.prototype; var _ICMInlineImage = new _ICMInlineImagePrototype(); _ICMInlineImagePrototype.prototype.willShowMedia = function(mediaId, mediaElement) /* PUBLIC */ { /* Do not allow user to show the image if this attribute is set. */ if (mediaElement.dataset.disabled === "true") { return false; } return this.superClass.willShowMedia.call(this, mediaId, mediaElement); }; _ICMInlineImagePrototype.prototype.entrypoint = function(payload, insertHTMLCallback) { /* Call super to insert the HTML for us. */ this.superClass.entrypoint.call(this, payload, insertHTMLCallback); /* Show the image when finishes loading. */ this.showImageWhenLoadedWithPayload(payload); }; _ICMInlineImagePrototype.prototype.showImageWhenLoadedWithPayload = function(payload) { this.showImageWhenLoaded(payload.uniqueIdentifier); }; _ICMInlineImagePrototype.prototype.showImageWhenLoaded = function(mediaId) { var imageContainer = document.getInlineMediaById(mediaId); if (!imageContainer) { console.error("Failed to find inline media element that matches ID: " + mediaId); return; } /* If the image is not already loaded, then we observe changes to that so that we only reveal once it is. */ var imageElement = imageContainer.querySelector("a .content"); var imageComplete = imageElement.complete; var imageCompleteCallback = (function() { if (imageContainer.dataset.disabled) { delete imageContainer.dataset.disabled; } imageElement.addEventListener("mousedown", InlineImageLiveResize.onMouseDown, false); this.show(mediaId); }).bind(this); if (imageComplete) { imageCompleteCallback(); } else { imageContainer.dataset.disabled = "true"; imageElement.addEventListener("load", { handleEvent: imageCompleteCallback }, false); } }; var _ICMInlineImage = new _ICMInlineImagePrototype(); ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineImage.mustache ================================================ ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineVideo.css ================================================ .inlineVideo, .inlineVideoService { overflow: auto; display: block; margin-top: 12px; margin-bottom: 12px; text-indent: 0; } .inlineVideo video { background-color: #000; } ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineVideo.js ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2017, 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ var _ICMInlineVideoPrototypeParent = InlineMediaPrototype; var _ICMInlineVideoPrototype = function() { _ICMInlineVideoPrototypeParent.call(this); } _ICMInlineVideoPrototype.prototype = Object.create(InlineMediaPrototype.prototype); _ICMInlineVideoPrototype.prototype.constructor = _ICMInlineVideoPrototype; _ICMInlineVideoPrototype.prototype.superClass = _ICMInlineVideoPrototypeParent.prototype; var _ICMInlineVideo = new _ICMInlineVideoPrototype(); _ICMInlineVideo.metadataLoadedCallback = function() { var video = event.target; /* Video start time */ var startTime = parseFloat(video.dataset.start); if (startTime > 0.0) { video.currentTime = startTime; } /* Video playback speed (rate) */ var playbackSpeed = parseFloat(video.dataset.speed); if (playbackSpeed !== 1.0) { video.playbackRate = playbackSpeed; } }; _ICMInlineVideo.dataLoadedCallback = function() { /* Loading data can change the height of the video once we know what it truly will be. We therefore have to tell the scroller to restore the scroll position. */ TextualScroller.restoreScrolledToBottom(); }; ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/ICMInlineVideo.mustache ================================================ × ================================================ FILE: XPC Services/Inline Content Loader/Resources/Modules/InlineImageLiveResize.js ================================================ /* ********************************************************************* * _____ _ _ * |_ _|____ _| |_ _ _ __ _| | * | |/ _ \ \/ / __| | | |/ _` | | * | | __/> <| |_| |_| | (_| | | * |_|\___/_/\_\\__|\__,_|\__,_|_| * * Copyright (c) 2014 Alex Sørlie. * Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors. * Please see Acknowledgements.pdf for additional information. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Textual, "Codeux Software, LLC", nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * *********************************************************************** */ /* ************************************************** */ /* */ /* DO NOT OVERRIDE ANYTHING BELOW THIS LINE */ /* */ /* ************************************************** */ var InlineImageLiveResize = (function () { function InlineImageLiveResize() { } InlineImageLiveResize.dragElement = null; InlineImageLiveResize.previousX = null; InlineImageLiveResize.previousY = null; InlineImageLiveResize.previousMouseActionWasForResizing = false; /* When the mouse down event is triggered on an element we set the target and record X,Y coordinates. using preventDefault we halt the default actions taken by the browser, the user can use the shift key to override InlineImageLiveResize behavior. */ InlineImageLiveResize.onMouseDown = function (e) { if (window.event.shiftKey === false) { InlineImageLiveResize.dragElement = e.target; InlineImageLiveResize.previousX = e.clientX; InlineImageLiveResize.previousY = e.clientY; var computedSize = window.getComputedStyle(e.target, null); InlineImageLiveResize.dragElement.style.height = computedSize.getPropertyValue("height"); InlineImageLiveResize.dragElement.style.width = computedSize.getPropertyValue("width"); InlineImageLiveResize.dragElement.style.maxWidth = null; e.preventDefault(); } }; /* The browser has given us a frame to our work on, we will compare the new coordinates of the mouse the old ones and resize the element accordingly */ InlineImageLiveResize.updateImage = function (x, y) { if (InlineImageLiveResize.dragElement === null || InlineImageLiveResize.dragElement === undefined) { return; } if (InlineImageLiveResize.previousMouseActionWasForResizing === false) { InlineImageLiveResize.previousMouseActionWasForResizing = true; } if (x > InlineImageLiveResize.previousX && y >= InlineImageLiveResize.previousY) { InlineImageLiveResize.dragElement.style.width = (InlineImageLiveResize.dragElement.offsetWidth + (x - InlineImageLiveResize.previousX)); InlineImageLiveResize.dragElement.style.height = "auto"; } else if (x < InlineImageLiveResize.previousX && y <= InlineImageLiveResize.previousY) { InlineImageLiveResize.dragElement.style.width = (InlineImageLiveResize.dragElement.offsetWidth - (InlineImageLiveResize.previousX - x)); InlineImageLiveResize.dragElement.style.height = "auto"; } else if (y > InlineImageLiveResize.previousY && x >= InlineImageLiveResize.previousX) { InlineImageLiveResize.dragElement.style.height = (InlineImageLiveResize.dragElement.offsetHeight + (y - InlineImageLiveResize.previousY)); InlineImageLiveResize.dragElement.style.width = "auto"; } else if (y < InlineImageLiveResize.previousY && x <= InlineImageLiveResize.previousX) { InlineImageLiveResize.dragElement.style.height = (InlineImageLiveResize.dragElement.offsetHeight - (InlineImageLiveResize.previousY - y)); InlineImageLiveResize.dragElement.style.width = "auto"; } InlineImageLiveResize.previousX = x; InlineImageLiveResize.previousY = y; }; /* Document state tracking. */ InlineImageLiveResize.onMouseDownGeneric = function (e) { InlineImageLiveResize.previousMouseActionWasForResizing = false; }; /* When mouse movement is done we check if the user has clicked on an element previously. We then request the next rendering frame of the browser to call our event to limit the amount of resize calculations done to 60fps. */ InlineImageLiveResize.onMouseMove = function (e) { if (InlineImageLiveResize.dragElement) { requestAnimationFrame(function () { InlineImageLiveResize.updateImage(e.clientX, e.clientY); }); e.preventDefault(); } }; /* When the user releases their mouse button we abort the drag action and reset all variables */ InlineImageLiveResize.onMouseUp = function (e) { if (InlineImageLiveResize.dragElement) { InlineImageLiveResize.dragElement = null; InlineImageLiveResize.previousX = null; InlineImageLiveResize.previousY = null; e.preventDefault(); } }; /* Called by image anchors to know whether to open link. */ InlineImageLiveResize.negateAnchorOpen = function () { if (InlineImageLiveResize.previousMouseActionWasForResizing === true) { return false; } else { return true; } }; return InlineImageLiveResize; })(); /* Bind to events */ document.addEventListener("mousedown", InlineImageLiveResize.onMouseDownGeneric, false); document.addEventListener("mousemove", InlineImageLiveResize.onMouseMove, false); document.addEventListener("mouseup", InlineImageLiveResize.onMouseUp, false); ================================================ FILE: XPC Services/Inline Content Loader/Resources/Property Lists/Info.plist ================================================ NSAppTransportSecurity NSAllowsArbitraryLoads CFBundleDisplayName Inline Content Loader CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleVersion 1.0.0 NSHumanReadableCopyright Copyright © 2017, 2018 Codeux Software, LLC. All rights reserved. XPCService ServiceType Application