Timecode message text entities

Languages in settings search
This commit is contained in:
Ilya Laktyushin 2019-03-23 14:31:03 +04:00
parent a2ee837531
commit dbfa8cc3ea
47 changed files with 4035 additions and 3486 deletions

View File

@ -94,7 +94,7 @@
09B4EE4D21A7B73800847FA6 /* PermissionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4C21A7B73800847FA6 /* PermissionController.swift */; }; 09B4EE4D21A7B73800847FA6 /* PermissionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4C21A7B73800847FA6 /* PermissionController.swift */; };
09B4EE4F21A7B75D00847FA6 /* PermissionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */; }; 09B4EE4F21A7B75D00847FA6 /* PermissionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */; };
09B4EE5221A7CC3E00847FA6 /* SolidRoundedButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */; }; 09B4EE5221A7CC3E00847FA6 /* SolidRoundedButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */; };
09B4EE5621A8149C00847FA6 /* PermissionInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5521A8149C00847FA6 /* PermissionInfoItem.swift */; }; 09B4EE5621A8149C00847FA6 /* ItemListInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5521A8149C00847FA6 /* ItemListInfoItem.swift */; };
09B4EE5E21AC626B00847FA6 /* PermissionContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */; }; 09B4EE5E21AC626B00847FA6 /* PermissionContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */; };
09B4EE6021AD4A0E00847FA6 /* InstantPageContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */; }; 09B4EE6021AD4A0E00847FA6 /* InstantPageContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */; };
09B4EE6221AD791600847FA6 /* InstantPageStoredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */; }; 09B4EE6221AD791600847FA6 /* InstantPageStoredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */; };
@ -1257,7 +1257,7 @@
09B4EE4C21A7B73800847FA6 /* PermissionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionController.swift; sourceTree = "<group>"; }; 09B4EE4C21A7B73800847FA6 /* PermissionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionController.swift; sourceTree = "<group>"; };
09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionControllerNode.swift; sourceTree = "<group>"; }; 09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionControllerNode.swift; sourceTree = "<group>"; };
09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidRoundedButtonNode.swift; sourceTree = "<group>"; }; 09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidRoundedButtonNode.swift; sourceTree = "<group>"; };
09B4EE5521A8149C00847FA6 /* PermissionInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionInfoItem.swift; sourceTree = "<group>"; }; 09B4EE5521A8149C00847FA6 /* ItemListInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListInfoItem.swift; sourceTree = "<group>"; };
09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionContentNode.swift; sourceTree = "<group>"; }; 09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionContentNode.swift; sourceTree = "<group>"; };
09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageContentNode.swift; sourceTree = "<group>"; }; 09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageContentNode.swift; sourceTree = "<group>"; };
09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageStoredState.swift; sourceTree = "<group>"; }; 09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageStoredState.swift; sourceTree = "<group>"; };
@ -2922,7 +2922,6 @@
D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */, D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */,
D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */, D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */,
D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */, D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */,
09B4EE5521A8149C00847FA6 /* PermissionInfoItem.swift */,
); );
name = Notifications; name = Notifications;
sourceTree = "<group>"; sourceTree = "<group>";
@ -4153,6 +4152,7 @@
D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */, D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */,
D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */, D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */,
D0BFAE5A20AB35D200793CF2 /* IconSwitchNode.swift */, D0BFAE5A20AB35D200793CF2 /* IconSwitchNode.swift */,
09B4EE5521A8149C00847FA6 /* ItemListInfoItem.swift */,
); );
name = Items; name = Items;
sourceTree = "<group>"; sourceTree = "<group>";
@ -5879,7 +5879,7 @@
09749BC521F0E024008FDDE9 /* StickersChatInputPanelItem.swift in Sources */, 09749BC521F0E024008FDDE9 /* StickersChatInputPanelItem.swift in Sources */,
D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */, D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */,
D0EC6DD01EB9F58900EBF1C3 /* HashtagChatInputContextPanelNode.swift in Sources */, D0EC6DD01EB9F58900EBF1C3 /* HashtagChatInputContextPanelNode.swift in Sources */,
09B4EE5621A8149C00847FA6 /* PermissionInfoItem.swift in Sources */, 09B4EE5621A8149C00847FA6 /* ItemListInfoItem.swift in Sources */,
D0EC6DD11EB9F58900EBF1C3 /* HashtagChatInputPanelItem.swift in Sources */, D0EC6DD11EB9F58900EBF1C3 /* HashtagChatInputPanelItem.swift in Sources */,
D0EC6DD21EB9F58900EBF1C3 /* MentionChatInputContextPanelNode.swift in Sources */, D0EC6DD21EB9F58900EBF1C3 /* MentionChatInputContextPanelNode.swift in Sources */,
D00701A22029F6D0006B9E34 /* TGMimeTypeMap.m in Sources */, D00701A22029F6D0006B9E34 /* TGMimeTypeMap.m in Sources */,

View File

@ -108,7 +108,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
break break
case .ignore: case .ignore:
return .fail return .fail
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .call, .openMessage: case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .call, .openMessage, .timecode:
return .waitForSingleTap return .waitForSingleTap
} }
} }
@ -301,15 +301,15 @@ final class ChatBotInfoItemNode: ListViewItemNode {
case .none, .ignore: case .none, .ignore:
break break
case let .url(url, _): case let .url(url, _):
item.controllerInteraction.longTap(.url(url)) item.controllerInteraction.longTap(.url(url), nil)
case let .peerMention(peerId, mention): case let .peerMention(peerId, mention):
item.controllerInteraction.longTap(.peerMention(peerId, mention)) item.controllerInteraction.longTap(.peerMention(peerId, mention), nil)
case let .textMention(name): case let .textMention(name):
item.controllerInteraction.longTap(.mention(name)) item.controllerInteraction.longTap(.mention(name), nil)
case let .botCommand(command): case let .botCommand(command):
item.controllerInteraction.longTap(.command(command)) item.controllerInteraction.longTap(.command(command), nil)
case let .hashtag(_, hashtag): case let .hashtag(_, hashtag):
item.controllerInteraction.longTap(.hashtag(hashtag)) item.controllerInteraction.longTap(.hashtag(hashtag), nil)
default: default:
break break
} }

View File

@ -860,7 +860,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
} }
}) })
} }
}, longTap: { [weak self] action in }, longTap: { [weak self] action, messageId in
if let strongSelf = self { if let strongSelf = self {
switch action { switch action {
case let .url(url): case let .url(url):
@ -1026,6 +1026,30 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
])]) ])])
strongSelf.chatDisplayNode.dismissInput() strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(actionSheet, in: .window(.root)) strongSelf.present(actionSheet, in: .window(.root))
case let .timecode(timecode, text):
guard let messageId = messageId else {
return
}
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: text),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.controllerInteraction?.seekToTimecode(messageId, timecode)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = text
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(actionSheet, in: .window(.root))
} }
} }
}, openCheckoutOrReceipt: { [weak self] messageId in }, openCheckoutOrReceipt: { [weak self] messageId in
@ -1192,6 +1216,24 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
})) }))
} }
} }
}, seekToTimecode: { [weak self] messageId, timestamp in
if let strongSelf = self {
let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId)
var completed = false
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !completed, let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.message.id == messageId, let (action, _, _, _, _) = itemNode.playMediaWithSound() {
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
action(Double(timestamp))
} else if let message = message {
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
}
completed = true
}
}
if !completed, let message = message {
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
}
}
}, requestMessageUpdate: { [weak self] id in }, requestMessageUpdate: { [weak self] id in
if let strongSelf = self { if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
@ -3287,7 +3329,25 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
return return
} }
strongSelf.videoUnmuteTooltipController?.dismiss() strongSelf.videoUnmuteTooltipController?.dismiss()
strongSelf.chatDisplayNode.playFirstMediaWithSound()
var actions: [(Bool, (Double?) -> Void)] = []
var hasUnconsumed = false
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() {
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
actions.insert((isUnconsumed, action), at: 0)
if !hasUnconsumed && isUnconsumed {
hasUnconsumed = true
}
}
}
}
for (isUnconsumed, action) in actions {
if (!hasUnconsumed || isUnconsumed) {
action(nil)
break
}
}
}) })
self.displayNodeDidLoad() self.displayNodeDidLoad()
@ -3511,7 +3571,13 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
} }
let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false
if self.validLayout != nil && orientation.isLandscape && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) { if self.validLayout != nil && orientation.isLandscape && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) {
self.chatDisplayNode.openCurrentPlayingWithSoundMedia() var completed = false
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !completed, let itemNode = itemNode as? ChatMessageItemView, let message = itemNode.item?.message, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled {
let _ = self.controllerInteraction?.openMessage(message, .landscape)
completed = true
}
}
} }
} }

View File

@ -35,6 +35,7 @@ public enum ChatControllerInteractionLongTapAction {
case peerMention(PeerId, String) case peerMention(PeerId, String)
case command(String) case command(String)
case hashtag(String) case hashtag(String)
case timecode(Double, String)
} }
public enum ChatControllerInteractionOpenMessageMode { public enum ChatControllerInteractionOpenMessageMode {
@ -42,6 +43,7 @@ public enum ChatControllerInteractionOpenMessageMode {
case stream case stream
case automaticPlayback case automaticPlayback
case landscape case landscape
case timecode(Double)
} }
struct ChatInterfacePollActionState: Equatable { struct ChatInterfacePollActionState: Equatable {
@ -75,7 +77,7 @@ public final class ChatControllerInteraction {
let navigationController: () -> NavigationController? let navigationController: () -> NavigationController?
let presentGlobalOverlayController: (ViewController, Any?) -> Void let presentGlobalOverlayController: (ViewController, Any?) -> Void
let callPeer: (PeerId) -> Void let callPeer: (PeerId) -> Void
let longTap: (ChatControllerInteractionLongTapAction) -> Void let longTap: (ChatControllerInteractionLongTapAction, MessageId?) -> Void
let openCheckoutOrReceipt: (MessageId) -> Void let openCheckoutOrReceipt: (MessageId) -> Void
let openSearch: () -> Void let openSearch: () -> Void
let setupReply: (MessageId) -> Void let setupReply: (MessageId) -> Void
@ -87,6 +89,7 @@ public final class ChatControllerInteraction {
let requestSelectMessagePollOption: (MessageId, Data) -> Void let requestSelectMessagePollOption: (MessageId, Data) -> Void
let openAppStorePage: () -> Void let openAppStorePage: () -> Void
let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
let seekToTimecode: (MessageId, Double) -> Void
let requestMessageUpdate: (MessageId) -> Void let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void let cancelInteractiveKeyboardGestures: () -> Void
@ -99,7 +102,7 @@ public final class ChatControllerInteraction {
var pollActionState: ChatInterfacePollActionState var pollActionState: ChatInterfacePollActionState
var searchTextHighightState: String? var searchTextHighightState: String?
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) { init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, MessageId?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (MessageId, Double) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) {
self.openMessage = openMessage self.openMessage = openMessage
self.openPeer = openPeer self.openPeer = openPeer
self.openPeerMention = openPeerMention self.openPeerMention = openPeerMention
@ -138,6 +141,7 @@ public final class ChatControllerInteraction {
self.requestSelectMessagePollOption = requestSelectMessagePollOption self.requestSelectMessagePollOption = requestSelectMessagePollOption
self.openAppStorePage = openAppStorePage self.openAppStorePage = openAppStorePage
self.displayMessageTooltip = displayMessageTooltip self.displayMessageTooltip = displayMessageTooltip
self.seekToTimecode = seekToTimecode
self.requestMessageUpdate = requestMessageUpdate self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
@ -152,7 +156,7 @@ public final class ChatControllerInteraction {
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in }, navigationController: { }, presentController: { _, _ in }, navigationController: {
return nil return nil
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
}, canSetupReply: { _ in }, canSetupReply: { _ in
return false return false
}, navigateToFirstDateMessage: { _ in }, navigateToFirstDateMessage: { _ in
@ -162,6 +166,7 @@ public final class ChatControllerInteraction {
}, requestSelectMessagePollOption: { _, _ in }, requestSelectMessagePollOption: { _, _ in
}, openAppStorePage: { }, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in }, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -1488,43 +1488,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.historyNode.prefetchManager.updateAutoDownloadSettings(settings) self.historyNode.prefetchManager.updateAutoDownloadSettings(settings)
} }
func playFirstMediaWithSound() {
var actions: [(CGFloat, Bool, () -> Void)] = []
var hasUnconsumed = false
self.historyNode.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() {
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
actions.insert((fraction, isUnconsumed, action), at: 0)
if !hasUnconsumed && isUnconsumed {
hasUnconsumed = true
}
}
}
}
for (_, isUnconsumed, action) in actions {
if (!hasUnconsumed || isUnconsumed) {
action()
break
}
}
}
func openCurrentPlayingWithSoundMedia() {
var result: (Message?, ListViewItemNode)?
self.historyNode.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled {
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
result = (itemNode.item?.message, itemNode)
}
}
}
if let (message, _) = result {
if let message = message {
let _ = self.controllerInteraction.openMessage(message, .landscape)
}
}
}
var isInputViewFocused: Bool { var isInputViewFocused: Bool {
if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
return inputPanelNode.isFocused return inputPanelNode.isFocused

View File

@ -248,7 +248,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
TelegramTextAttributes.PeerMention, TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand, TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag] TelegramTextAttributes.Hashtag,
TelegramTextAttributes.Timecode]
for attribute in highlightedAttributes { for attribute in highlightedAttributes {
if let _ = attributes[NSAttributedStringKey(rawValue: attribute)] { if let _ = attributes[NSAttributedStringKey(rawValue: attribute)] {
@ -308,10 +309,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
private func actionForAttributes(_ attributes: [NSAttributedStringKey: Any]) -> GalleryControllerInteractionTapAction? { private func actionForAttributes(_ attributes: [NSAttributedStringKey: Any]) -> GalleryControllerInteractionTapAction? {
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String { if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
// var concealed = true
// if let attributeText = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
// concealed = !doesUrlMatchText(url: url, text: attributeText)
// }
return .url(url: url, concealed: false) return .url(url: url, concealed: false)
} else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention) return .peerMention(peerMention.peerId, peerMention.mention)
@ -321,6 +318,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
return .botCommand(botCommand) return .botCommand(botCommand)
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag) return .hashtag(hashtag.peerName, hashtag.hashtag)
} else if let timecode = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
return .timecode(timecode.time, timecode.text)
} else { } else {
return nil return nil
} }

View File

@ -1017,7 +1017,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
} }
func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? { func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return self.contentImageNode?.playMediaWithSound() return self.contentImageNode?.playMediaWithSound()
} }
} }

View File

@ -73,6 +73,7 @@ enum ChatMessageBubbleContentTapAction {
case wallpaper case wallpaper
case call(PeerId) case call(PeerId)
case openMessage case openMessage
case timecode(Double, String)
case ignore case ignore
} }
@ -147,7 +148,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) { func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) {
} }
func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? { func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return nil return nil
} }

View File

@ -257,7 +257,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
break break
case .ignore: case .ignore:
return .fail return .fail
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .call, .openMessage: case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .call, .openMessage, .timecode:
return .waitForSingleTap return .waitForSingleTap
} }
} }
@ -1727,6 +1727,37 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
let _ = item.controllerInteraction.openMessage(item.message, .default) let _ = item.controllerInteraction.openMessage(item.message, .default)
} }
break loop break loop
case let .timecode(timecode, _):
foundTapAction = true
if let item = self.item {
var messageId: MessageId?
for media in item.message.media {
if let file = media as? TelegramMediaFile, file.duration != nil {
messageId = item.message.id
}
}
if messageId == nil {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
if let replyMessage = item.message.associatedMessages[attribute.messageId] {
for media in replyMessage.media {
if let file = media as? TelegramMediaFile, file.duration != nil {
messageId = replyMessage.id
break
}
}
}
}
}
}
if messageId == nil {
messageId = item.message.id
}
if let messageId = messageId {
item.controllerInteraction.seekToTimecode(messageId, timecode)
}
}
break loop
} }
} }
if !foundTapAction { if !foundTapAction {
@ -1734,6 +1765,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
} }
case .longTap, .doubleTap: case .longTap, .doubleTap:
if let item = self.item, self.backgroundNode.frame.contains(location) { if let item = self.item, self.backgroundNode.frame.contains(location) {
let messageId = item.message.id
var foundTapAction = false var foundTapAction = false
var tapMessage: Message? = item.content.firstMessage var tapMessage: Message? = item.content.firstMessage
var selectAll = true var selectAll = true
@ -1750,23 +1783,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
break break
case let .url(url, _): case let .url(url, _):
foundTapAction = true foundTapAction = true
item.controllerInteraction.longTap(.url(url)) item.controllerInteraction.longTap(.url(url), messageId)
break loop break loop
case let .peerMention(peerId, mention): case let .peerMention(peerId, mention):
foundTapAction = true foundTapAction = true
item.controllerInteraction.longTap(.peerMention(peerId, mention)) item.controllerInteraction.longTap(.peerMention(peerId, mention), messageId)
break loop break loop
case let .textMention(name): case let .textMention(name):
foundTapAction = true foundTapAction = true
item.controllerInteraction.longTap(.mention(name)) item.controllerInteraction.longTap(.mention(name), messageId)
break loop break loop
case let .botCommand(command): case let .botCommand(command):
foundTapAction = true foundTapAction = true
item.controllerInteraction.longTap(.command(command)) item.controllerInteraction.longTap(.command(command), messageId)
break loop break loop
case let .hashtag(_, hashtag): case let .hashtag(_, hashtag):
foundTapAction = true foundTapAction = true
item.controllerInteraction.longTap(.hashtag(hashtag)) item.controllerInteraction.longTap(.hashtag(hashtag), messageId)
break loop break loop
case .instantPage: case .instantPage:
break break
@ -1777,6 +1810,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
case .openMessage: case .openMessage:
foundTapAction = false foundTapAction = false
break break
case let .timecode(timecode, text):
foundTapAction = true
item.controllerInteraction.longTap(.timecode(timecode, text), messageId)
break loop
} }
} }
if !foundTapAction, let tapMessage = tapMessage { if !foundTapAction, let tapMessage = tapMessage {
@ -1932,7 +1969,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
} }
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? { override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
for contentNode in self.contentNodes { for contentNode in self.contentNodes {
if let playMediaWithSound = contentNode.playMediaWithSound() { if let playMediaWithSound = contentNode.playMediaWithSound() {
return playMediaWithSound return playMediaWithSound

View File

@ -709,7 +709,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} }
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? { override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return self.interactiveVideoNode.playMediaWithSound() return self.interactiveVideoNode.playMediaWithSound()
} }
} }

View File

@ -504,7 +504,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
if strongSelf.consumableContentNode.image !== consumableContentIcon { if strongSelf.consumableContentNode.image !== consumableContentIcon {
strongSelf.consumableContentNode.image = consumableContentIcon strongSelf.consumableContentNode.image = consumableContentIcon
} }
strongSelf.consumableContentNode.frame = CGRect(origin: CGPoint(x: descriptionFrame.maxX + 2.0, y: descriptionFrame.minY + 5.0), size: consumableContentIcon.size) strongSelf.consumableContentNode.frame = CGRect(origin: CGPoint(x: descriptionFrame.maxX + 5.0, y: descriptionFrame.minY + 5.0), size: consumableContentIcon.size)
} else if strongSelf.consumableContentNode.supernode != nil { } else if strongSelf.consumableContentNode.supernode != nil {
strongSelf.consumableContentNode.removeFromSupernode() strongSelf.consumableContentNode.removeFromSupernode()
} }

View File

@ -749,7 +749,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
} }
} }
func playMediaWithSound() -> (action: () -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
if let item = self.item { if let item = self.item {
var isUnconsumed = false var isUnconsumed = false
for attribute in item.message.attributes { for attribute in item.message.attributes {
@ -761,7 +761,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
} }
} }
return ({ return ({ _ in
if !self.infoBackgroundNode.alpha.isZero { if !self.infoBackgroundNode.alpha.isZero {
let _ = (item.context.sharedContext.mediaManager.globalMediaPlayerState let _ = (item.context.sharedContext.mediaManager.globalMediaPlayerState
|> take(1) |> take(1)

View File

@ -1210,7 +1210,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
}) })
} }
func playMediaWithSound() -> (action: () -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
var isAnimated = false var isAnimated = false
if let file = self.media as? TelegramMediaFile, file.isAnimated { if let file = self.media as? TelegramMediaFile, file.isAnimated {
isAnimated = true isAnimated = true
@ -1224,7 +1224,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} }
if let videoNode = self.videoNode, let context = self.context, (self.automaticPlayback ?? false) && !isAnimated { if let videoNode = self.videoNode, let context = self.context, (self.automaticPlayback ?? false) && !isAnimated {
return ({ return ({ timecode in
if let timecode = timecode {
context.sharedContext.mediaManager.playlistControl(.playback(.pause))
videoNode.playOnceWithSound(playAndRecord: false, seek: .timecode(timecode), actionAtEnd: actionAtEnd)
} else {
let _ = (context.sharedContext.mediaManager.globalMediaPlayerState let _ = (context.sharedContext.mediaManager.globalMediaPlayerState
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { playlistStateAndType in |> deliverOnMainQueue).start(next: { playlistStateAndType in
@ -1240,9 +1244,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} }
} }
if canPlay { if canPlay {
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: actionAtEnd) videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: actionAtEnd)
} }
}) })
}
}, (self.playerStatus?.soundEnabled ?? false), false, false, self.badgeNode) }, (self.playerStatus?.soundEnabled ?? false), false, false, self.badgeNode)
} else { } else {
return nil return nil

View File

@ -697,7 +697,7 @@ public class ChatMessageItemView: ListViewItemNode {
func updateAutomaticMediaDownloadSettings() { func updateAutomaticMediaDownloadSettings() {
} }
func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? { func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return nil return nil
} }
@ -757,7 +757,7 @@ public class ChatMessageItemView: ListViewItemNode {
if let item = self.item { if let item = self.item {
switch button.action { switch button.action {
case let .url(url): case let .url(url):
item.controllerInteraction.longTap(.url(url)) item.controllerInteraction.longTap(.url(url), item.message.id)
default: default:
break break
} }

View File

@ -320,7 +320,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
return mediaHidden return mediaHidden
} }
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? { override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return self.interactiveImageNode.playMediaWithSound() return self.interactiveImageNode.playMediaWithSound()
} }

View File

@ -139,9 +139,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let attributedText: NSAttributedString let attributedText: NSAttributedString
var messageEntities: [MessageTextEntity]? var messageEntities: [MessageTextEntity]?
var mediaDuration: Double? = nil
var isUnsupportedMedia = false var isUnsupportedMedia = false
for media in item.message.media { for media in item.message.media {
if let _ = media as? TelegramMediaUnsupported { if let file = media as? TelegramMediaFile, let duration = file.duration {
mediaDuration = Double(duration)
}
else if media is TelegramMediaUnsupported {
isUnsupportedMedia = true isUnsupportedMedia = true
} }
} }
@ -154,10 +158,18 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute { if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities messageEntities = attribute.entities
} else if mediaDuration == nil, let attribute = attribute as? ReplyMessageAttribute {
if let replyMessage = item.message.associatedMessages[attribute.messageId] {
for media in replyMessage.media {
if let file = media as? TelegramMediaFile, let duration = file.duration {
mediaDuration = Double(duration)
break break
} }
} }
} }
}
}
}
var entities: [MessageTextEntity]? var entities: [MessageTextEntity]?
@ -166,8 +178,17 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
entities = cached.entities entities = cached.entities
} else { } else {
entities = messageEntities entities = messageEntities
if entities == nil && mediaDuration != nil {
entities = []
}
if let entitiesValue = entities { if let entitiesValue = entities {
if let result = addLocallyGeneratedEntities(rawText, enabledTypes: .all, entities: entitiesValue) { var enabledTypes: EnabledEntityTypes = .all
if mediaDuration != nil {
enabledTypes.insert(.timecode)
}
if let result = addLocallyGeneratedEntities(rawText, enabledTypes: enabledTypes, entities: entitiesValue, mediaDuration: mediaDuration) {
entities = result entities = result
} }
} else { } else {
@ -372,6 +393,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return .botCommand(botCommand) return .botCommand(botCommand)
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag) return .hashtag(hashtag.peerName, hashtag.hashtag)
} else if let timecode = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
return .timecode(timecode.time, timecode.text)
} else { } else {
return .none return .none
} }
@ -391,7 +414,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
TelegramTextAttributes.PeerMention, TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand, TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag TelegramTextAttributes.Hashtag,
TelegramTextAttributes.Timecode
] ]
for name in possibleNames { for name in possibleNames {
if let _ = attributes[NSAttributedStringKey(rawValue: name)] { if let _ = attributes[NSAttributedStringKey(rawValue: name)] {

View File

@ -369,7 +369,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
} }
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? { override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return self.contentNode.playMediaWithSound() return self.contentNode.playMediaWithSound()
} }

View File

@ -220,7 +220,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, presentController: { _, _ in }, presentController: { _, _ in
}, navigationController: { [weak self] in }, navigationController: { [weak self] in
return self?.getNavigationController() return self?.getNavigationController()
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action, messageId in
if let strongSelf = self { if let strongSelf = self {
switch action { switch action {
case let .url(url): case let .url(url):
@ -347,6 +347,29 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}) })
])]) ])])
strongSelf.presentController(actionSheet, nil) strongSelf.presentController(actionSheet, nil)
case let .timecode(timecode, text):
guard let messageId = messageId else {
return
}
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: text),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.controllerInteraction?.seekToTimecode(messageId, timecode)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = text
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
} }
} }
}, openCheckoutOrReceipt: { _ in }, openCheckoutOrReceipt: { _ in
@ -364,6 +387,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
strongSelf.context.sharedContext.applicationBindings.openAppStorePage() strongSelf.context.sharedContext.applicationBindings.openAppStorePage()
} }
}, displayMessageTooltip: { _, _, _, _ in }, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -163,7 +163,7 @@ enum ContactListPeer: Equatable {
private enum ContactListNodeEntry: Comparable, Identifiable { private enum ContactListNodeEntry: Comparable, Identifiable {
case search(PresentationTheme, PresentationStrings) case search(PresentationTheme, PresentationStrings)
case sort(PresentationTheme, PresentationStrings, ContactsSortOrder) case sort(PresentationTheme, PresentationStrings, ContactsSortOrder)
case permissionInfo(PresentationTheme, PresentationStrings, Bool) case permissionInfo(PresentationTheme, String, String, Bool)
case permissionEnable(PresentationTheme, String) case permissionEnable(PresentationTheme, String)
case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings) case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings)
case peer(Int, ContactListPeer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool) case peer(Int, ContactListPeer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool)
@ -204,8 +204,8 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
return ContactListActionItem(theme: theme, title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: { return ContactListActionItem(theme: theme, title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: {
interaction.openSortMenu() interaction.openSortMenu()
}) })
case let .permissionInfo(theme, strings, suppressed): case let .permissionInfo(theme, title, text, suppressed):
return PermissionInfoItem(theme: theme, strings: strings, subject: .contacts, type: .denied, style: .plain, suppressed: suppressed, close: { return InfoListItem(theme: theme, title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : {
interaction.suppressWarning() interaction.suppressWarning()
}) })
case let .permissionEnable(theme, text): case let .permissionEnable(theme, text):
@ -250,8 +250,8 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .permissionInfo(lhsTheme, lhsStrings, lhsSuppressed): case let .permissionInfo(lhsTheme, lhsTitle, lhsText, lhsSuppressed):
if case let .permissionInfo(rhsTheme, rhsStrings, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsSuppressed == rhsSuppressed { if case let .permissionInfo(rhsTheme, rhsTitle, rhsText, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSuppressed == rhsSuppressed {
return true return true
} else { } else {
return false return false
@ -441,13 +441,15 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer]
if #available(iOSApplicationExtension 10.0, *) { if #available(iOSApplicationExtension 10.0, *) {
let (suppressed, syncDisabled) = warningSuppressed let (suppressed, syncDisabled) = warningSuppressed
if !peers.isEmpty && !syncDisabled { if !peers.isEmpty && !syncDisabled {
let title = strings.Contacts_PermissionsTitle
let text = strings.Contacts_PermissionsText
switch authorizationStatus { switch authorizationStatus {
case .denied: case .denied:
entries.append(.permissionInfo(theme, strings, suppressed)) entries.append(.permissionInfo(theme, title, text, suppressed))
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0)) entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0))
addHeader = true addHeader = true
case .notDetermined: case .notDetermined:
entries.append(.permissionInfo(theme, strings, false)) entries.append(.permissionInfo(theme, title, text, false))
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllow_v0)) entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllow_v0))
addHeader = true addHeader = true
default: default:

View File

@ -131,7 +131,7 @@ private func galleryMessageCaptionText(_ message: Message) -> String {
return message.text return message.text
} }
func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }) -> GalleryItem? { func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }) -> GalleryItem? {
switch entry { switch entry {
case let .MessageEntry(message, _, location, _, _): case let .MessageEntry(message, _, location, _, _):
if let (media, mediaImage) = mediaForMessage(message: message) { if let (media, mediaImage) = mediaForMessage(message: message) {
@ -157,8 +157,14 @@ func galleryItemForEntry(context: AccountContext, presentationData: Presentation
break break
} }
} }
let caption = galleryCaptionStringWithAppliedEntities(galleryMessageCaptionText(message), entities: entities)
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions) let text = galleryMessageCaptionText(message)
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) {
entities = result
}
let caption = galleryCaptionStringWithAppliedEntities(text, entities: entities)
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions)
} else { } else {
if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" {
var pixelsCount: Int = 0 var pixelsCount: Int = 0
@ -194,7 +200,7 @@ func galleryItemForEntry(context: AccountContext, presentationData: Presentation
} }
} }
if let content = content { if let content = content {
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: performAction, openActionOptions: openActionOptions) return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, performAction: performAction, openActionOptions: openActionOptions)
} else { } else {
return nil return nil
} }
@ -251,6 +257,11 @@ enum GalleryControllerInteractionTapAction {
case peerMention(PeerId, String) case peerMention(PeerId, String)
case botCommand(String) case botCommand(String)
case hashtag(String?, String) case hashtag(String?, String)
case timecode(Double, String)
}
public enum GalleryControllerItemNodeAction {
case timecode(Double)
} }
final class GalleryControllerActionInteraction { final class GalleryControllerActionInteraction {
@ -298,6 +309,7 @@ class GalleryController: ViewController {
var temporaryDoNotWaitForReady = false var temporaryDoNotWaitForReady = false
private let fromPlayingVideo: Bool private let fromPlayingVideo: Bool
private let landscape: Bool private let landscape: Bool
private let timecode: Double?
private let accountInUseDisposable = MetaDisposable() private let accountInUseDisposable = MetaDisposable()
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
@ -323,7 +335,7 @@ class GalleryController: ViewController {
private var performAction: (GalleryControllerInteractionTapAction) -> Void private var performAction: (GalleryControllerInteractionTapAction) -> Void
private var openActionOptions: (GalleryControllerInteractionTapAction) -> Void private var openActionOptions: (GalleryControllerInteractionTapAction) -> Void
init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) { init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) {
self.context = context self.context = context
self.source = source self.source = source
self.replaceRootController = replaceRootController self.replaceRootController = replaceRootController
@ -332,6 +344,7 @@ class GalleryController: ViewController {
self.streamVideos = streamSingleVideo self.streamVideos = streamSingleVideo
self.fromPlayingVideo = fromPlayingVideo self.fromPlayingVideo = fromPlayingVideo
self.landscape = landscape self.landscape = landscape
self.timecode = timecode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -438,7 +451,7 @@ class GalleryController: ViewController {
if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == strongSelf.centralEntryStableId { if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == strongSelf.centralEntryStableId {
isCentral = true isCentral = true
} }
if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions) { if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions) {
if isCentral { if isCentral {
centralItemIndex = items.count centralItemIndex = items.count
} }
@ -536,7 +549,10 @@ class GalleryController: ViewController {
performActionImpl = { [weak self] action in performActionImpl = { [weak self] action in
if let strongSelf = self { if let strongSelf = self {
if case .timecode = action {
} else {
strongSelf.dismiss(forceAway: false) strongSelf.dismiss(forceAway: false)
}
switch action { switch action {
case let .url(url, concealed): case let .url(url, concealed):
strongSelf.actionInteraction?.openUrl(url, concealed) strongSelf.actionInteraction?.openUrl(url, concealed)
@ -548,6 +564,8 @@ class GalleryController: ViewController {
strongSelf.actionInteraction?.openBotCommand(command) strongSelf.actionInteraction?.openBotCommand(command)
case let .hashtag(peerName, hashtag): case let .hashtag(peerName, hashtag):
strongSelf.actionInteraction?.openHashtag(peerName, hashtag) strongSelf.actionInteraction?.openHashtag(peerName, hashtag)
case let .timecode(timecode, _):
strongSelf.galleryNode.pager.centralItemNode()?.processAction(.timecode(timecode))
} }
} }
} }
@ -698,6 +716,28 @@ class GalleryController: ViewController {
]) ])
]) ])
strongSelf.present(actionSheet, in: .window(.root)) strongSelf.present(actionSheet, in: .window(.root))
case let .timecode(timecode, text):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: text),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
strongSelf.galleryNode.pager.centralItemNode()?.processAction(.timecode(timecode))
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = text
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.present(actionSheet, in: .window(.root))
} }
} }
} }
@ -817,8 +857,12 @@ class GalleryController: ViewController {
var items: [GalleryItem] = [] var items: [GalleryItem] = []
var centralItemIndex: Int? var centralItemIndex: Int?
for entry in self.entries { for entry in self.entries {
if let item = galleryItemForEntry(context: context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: self.fromPlayingVideo, landscape: self.landscape, performAction: self.performAction, openActionOptions: self.openActionOptions) { var isCentral = false
if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == self.centralEntryStableId { if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == self.centralEntryStableId {
isCentral = true
}
if let item = galleryItemForEntry(context: context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, performAction: self.performAction, openActionOptions: self.openActionOptions) {
if isCentral {
centralItemIndex = items.count centralItemIndex = items.count
} }
items.append(item) items.append(item)

View File

@ -69,6 +69,9 @@ open class GalleryItemNode: ASDisplayNode {
open func activateAsInitial() { open func activateAsInitial() {
} }
open func processAction(_ action: GalleryControllerItemNodeAction) {
}
open func visibilityUpdated(isVisible: Bool) { open func visibilityUpdated(isVisible: Bool) {
} }

View File

@ -27,11 +27,28 @@ private let externalIdentifierDelimiterSet: CharacterSet = {
set.remove(".") set.remove(".")
return set return set
}() }()
private let timecodeDelimiterSet: CharacterSet = {
var set = CharacterSet.punctuationCharacters
set.formUnion(CharacterSet.whitespacesAndNewlines)
set.remove(":")
return set
}()
private let validTimecodeSet: CharacterSet = {
var set = CharacterSet(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
set.insert(":")
return set
}()
struct ApplicationSpecificEntityType {
public static let Timecode: Int32 = 1
}
private enum CurrentEntityType { private enum CurrentEntityType {
case command case command
case mention case mention
case hashtag case hashtag
case phoneNumber
case timecode
var type: EnabledEntityTypes { var type: EnabledEntityTypes {
switch self { switch self {
@ -41,6 +58,10 @@ private enum CurrentEntityType {
return .mention return .mention
case .hashtag: case .hashtag:
return .hashtag return .hashtag
case .phoneNumber:
return .phoneNumber
case .timecode:
return .timecode
} }
} }
} }
@ -57,12 +78,13 @@ public struct EnabledEntityTypes: OptionSet {
public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2) public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2)
public static let url = EnabledEntityTypes(rawValue: 1 << 3) public static let url = EnabledEntityTypes(rawValue: 1 << 3)
public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4) public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4)
public static let external = EnabledEntityTypes(rawValue: 1 << 5) public static let timecode = EnabledEntityTypes(rawValue: 1 << 5)
public static let external = EnabledEntityTypes(rawValue: 1 << 6)
public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber] public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber]
} }
private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range<String.UTF16View.Index>, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity]) { private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range<String.UTF16View.Index>, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity], mediaDuration: Double? = nil) {
if !enabledTypes.contains(type.type) { if !enabledTypes.contains(type.type) {
return return
} }
@ -83,9 +105,20 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType,
entityType = .Mention entityType = .Mention
case .hashtag: case .hashtag:
entityType = .Hashtag entityType = .Hashtag
case .phoneNumber:
entityType = .PhoneNumber
case .timecode:
entityType = .Custom(type: ApplicationSpecificEntityType.Timecode)
} }
if case .timecode = type, let mediaDuration = mediaDuration, let timecode = parseTimecodeString(String(utf16[range])) {
if timecode <= mediaDuration {
entities.append(MessageTextEntity(range: indexRange, type: entityType)) entities.append(MessageTextEntity(range: indexRange, type: entityType))
} }
} else {
entities.append(MessageTextEntity(range: indexRange, type: entityType))
}
}
} }
func generateChatInputTextEntities(_ text: NSAttributedString) -> [MessageTextEntity] { func generateChatInputTextEntities(_ text: NSAttributedString) -> [MessageTextEntity] {
@ -202,6 +235,8 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
} }
currentEntity = nil currentEntity = nil
} }
default:
break
} }
} }
} }
@ -216,23 +251,34 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
return entities return entities
} }
func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityTypes, entities: [MessageTextEntity]) -> [MessageTextEntity]? { func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityTypes, entities: [MessageTextEntity], mediaDuration: Double? = nil) -> [MessageTextEntity]? {
var resultEntities = entities var resultEntities = entities
var hasDigits = false var hasDigits = false
if enabledTypes.contains(.phoneNumber) { var hasColons = false
let detectPhoneNumbers = enabledTypes.contains(.phoneNumber)
let detectTimecodes = enabledTypes.contains(.timecode)
if detectPhoneNumbers || detectTimecodes {
loop: for c in text.utf16 { loop: for c in text.utf16 {
if let scalar = UnicodeScalar(c) { if let scalar = UnicodeScalar(c) {
if scalar >= "0" && scalar <= "9" { if scalar >= "0" && scalar <= "9" {
hasDigits = true hasDigits = true
if !detectTimecodes || hasColons {
break loop break loop
} }
} else if scalar == ":" {
hasColons = true
if !detectPhoneNumbers || hasDigits {
break loop
}
}
} }
} }
} }
if hasDigits { if hasDigits {
if let phoneNumberDetector = phoneNumberDetector, enabledTypes.contains(.phoneNumber) { if let phoneNumberDetector = phoneNumberDetector, detectPhoneNumbers {
let utf16 = text.utf16 let utf16 = text.utf16
phoneNumberDetector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in phoneNumberDetector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in
if let result = result { if let result = result {
@ -240,22 +286,55 @@ func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityType
let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text) let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text)
let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text) let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text)
if let lowerBound = lowerBound, let upperBound = upperBound { if let lowerBound = lowerBound, let upperBound = upperBound {
let indexRange: Range<Int> = utf16.distance(from: text.startIndex, to: lowerBound) ..< utf16.distance(from: text.startIndex, to: upperBound) commitEntity(utf16, .phoneNumber, lowerBound ..< upperBound, enabledTypes, &resultEntities)
var overlaps = false
for entity in resultEntities {
if entity.range.overlaps(indexRange) {
overlaps = true
break
}
}
if !overlaps {
resultEntities.append(MessageTextEntity(range: indexRange, type: .PhoneNumber))
}
} }
} }
} }
}) })
} }
if hasColons && detectTimecodes {
let utf16 = text.utf16
let delimiterSet = timecodeDelimiterSet
var index = utf16.startIndex
var currentEntity: (CurrentEntityType, Range<String.UTF16View.Index>)?
var previousScalar: UnicodeScalar?
while index != utf16.endIndex {
let c = utf16[index]
let scalar = UnicodeScalar(c)
var notFound = true
if let scalar = scalar {
if validTimecodeSet.contains(scalar) {
notFound = false
if let (type, range) = currentEntity, type == .timecode {
currentEntity = (.timecode, range.lowerBound ..< utf16.index(after: index))
} else if previousScalar == nil || CharacterSet.whitespacesAndNewlines.contains(previousScalar!) {
currentEntity = (.timecode, index ..< index)
}
}
if notFound {
if let (type, range) = currentEntity {
switch type {
case .timecode:
if delimiterSet.contains(scalar) {
commitEntity(utf16, type, range, enabledTypes, &resultEntities, mediaDuration: mediaDuration)
currentEntity = nil
}
default:
break
}
}
}
}
index = utf16.index(after: index)
previousScalar = scalar
}
if let (type, range) = currentEntity {
commitEntity(utf16, type, range, enabledTypes, &resultEntities, mediaDuration: mediaDuration)
}
}
} }
if resultEntities.count != entities.count { if resultEntities.count != entities.count {
@ -264,3 +343,25 @@ func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityType
return nil return nil
} }
} }
func parseTimecodeString(_ string: String?) -> Double? {
if let string = string, string.rangeOfCharacter(from: validTimecodeSet.inverted) == nil {
let components = string.components(separatedBy: ":")
if components.count > 1 && components.count <= 3 {
if components.count == 3 {
if let hours = Int(components[0]), let minutes = Int(components[1]), let seconds = Int(components[2]) {
if hours >= 0 && hours < 48 && minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60 {
return Double(seconds) + Double(minutes) * 60.0 + Double(hours) * 60.0 * 60.0
}
}
} else if components.count == 2 {
if let minutes = Int(components[0]), let seconds = Int(components[1]) {
if minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60 {
return Double(seconds) + Double(minutes) * 60.0
}
}
}
}
}
return nil
}

View File

@ -22,6 +22,7 @@ final class InstantPageController: ViewController {
} }
private var webpageDisposable: Disposable? private var webpageDisposable: Disposable?
private var storedStateDisposable: Disposable?
private var settings: InstantPagePresentationSettings? private var settings: InstantPagePresentationSettings?
private var settingsDisposable: Disposable? private var settingsDisposable: Disposable?
@ -79,6 +80,7 @@ final class InstantPageController: ViewController {
deinit { deinit {
self.webpageDisposable?.dispose() self.webpageDisposable?.dispose()
self.storedStateDisposable?.dispose()
self.settingsDisposable?.dispose() self.settingsDisposable?.dispose()
} }
@ -109,7 +111,7 @@ final class InstantPageController: ViewController {
} }
}) })
let _ = (instantPageStoredState(postbox: self.context.account.postbox, webPage: self.webPage) self.storedStateDisposable = (instantPageStoredState(postbox: self.context.account.postbox, webPage: self.webPage)
|> deliverOnMainQueue).start(next: { [weak self] state in |> deliverOnMainQueue).start(next: { [weak self] state in
if let strongSelf = self { if let strongSelf = self {
strongSelf.controllerNode.updateWebPage(strongSelf.webPage, anchor: strongSelf.anchor, state: state) strongSelf.controllerNode.updateWebPage(strongSelf.webPage, anchor: strongSelf.anchor, state: state)

View File

@ -203,6 +203,10 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.updateNavigationBar() self.updateNavigationBar()
self.recursivelyEnsureDisplaySynchronously(true) self.recursivelyEnsureDisplaySynchronously(true)
if let layout = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: .immediate)
}
} }
} }
} }
@ -359,9 +363,11 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
if didSetScrollOffset { if didSetScrollOffset {
self.previousContentOffset = contentOffset self.previousContentOffset = contentOffset
self.updateNavigationBar() self.updateNavigationBar()
if self.currentLayout != nil {
self.setupScrollOffsetOnLayout = false self.setupScrollOffsetOnLayout = false
} }
} }
}
if shouldUpdateVisibleItems { if shouldUpdateVisibleItems {
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
} }

View File

@ -163,7 +163,7 @@ class InstantPageGalleryController: ViewController {
private var innerOpenUrl: (InstantPageUrlItem) -> Void private var innerOpenUrl: (InstantPageUrlItem) -> Void
private var openUrlOptions: (InstantPageUrlItem) -> Void private var openUrlOptions: (InstantPageUrlItem) -> Void
init(context: AccountContext, webPage: TelegramMediaWebpage, message: Message? = nil, entries: [InstantPageGalleryEntry], centralIndex: Int, fromPlayingVideo: Bool = false, landscape: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?) { init(context: AccountContext, webPage: TelegramMediaWebpage, message: Message? = nil, entries: [InstantPageGalleryEntry], centralIndex: Int, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?) {
self.context = context self.context = context
self.webPage = webPage self.webPage = webPage
self.message = message self.message = message

View File

@ -3,30 +3,37 @@ import Display
import AsyncDisplayKit import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
class PermissionInfoItem: ListViewItem { enum InfoListItemText {
case plain(String)
case markdown(String)
}
enum InfoListItemLinkAction {
case tap(String)
}
class InfoListItem: ListViewItem {
let selectable: Bool = false let selectable: Bool = false
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let title: String
let subject: DeviceAccessSubject let text: InfoListItemText
let type: AccessType
let style: ItemListStyle let style: ItemListStyle
let suppressed: Bool let linkAction: ((InfoListItemLinkAction) -> Void)?
let close: () -> Void let closeAction: (() -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings, subject: DeviceAccessSubject, type: AccessType, style: ItemListStyle, suppressed: Bool, close: @escaping () -> Void) { init(theme: PresentationTheme, title: String, text: InfoListItemText, style: ItemListStyle, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) {
self.theme = theme self.theme = theme
self.strings = strings self.title = title
self.subject = subject self.text = text
self.type = type
self.style = style self.style = style
self.suppressed = suppressed self.linkAction = linkAction
self.close = close self.closeAction = closeAction
} }
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async { async {
let node = PermissionInfoItemNode() let node = InfoItemNode()
let (layout, apply) = node.asyncLayout()(self, params, nil) let (layout, apply) = node.asyncLayout()(self, params, nil)
node.contentSize = layout.contentSize node.contentSize = layout.contentSize
@ -42,7 +49,7 @@ class PermissionInfoItem: ListViewItem {
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async { Queue.mainQueue().async {
if let nodeValue = node() as? PermissionInfoItemNode { if let nodeValue = node() as? InfoItemNode {
let makeLayout = nodeValue.asyncLayout() let makeLayout = nodeValue.asyncLayout()
async { async {
@ -58,17 +65,17 @@ class PermissionInfoItem: ListViewItem {
} }
} }
class PermissionInfoItemListItem: PermissionInfoItem, ItemListItem { class ItemListInfoItem: InfoListItem, ItemListItem {
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
init(theme: PresentationTheme, strings: PresentationStrings, subject: DeviceAccessSubject, type: AccessType, style: ItemListStyle, sectionId: ItemListSectionId, suppressed: Bool, close: @escaping () -> Void) { init(theme: PresentationTheme, title: String, text: InfoListItemText, style: ItemListStyle, sectionId: ItemListSectionId, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) {
self.sectionId = sectionId self.sectionId = sectionId
super.init(theme: theme, strings: strings, subject: subject, type: type, style: style, suppressed: suppressed, close: close) super.init(theme: theme, title: title, text: text, style: style, linkAction: linkAction, closeAction: closeAction)
} }
override func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { override func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async { async {
let node = PermissionInfoItemNode() let node = InfoItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize node.contentSize = layout.contentSize
@ -84,7 +91,7 @@ class PermissionInfoItemListItem: PermissionInfoItem, ItemListItem {
override func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { override func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async { Queue.mainQueue().async {
if let nodeValue = node() as? PermissionInfoItemNode { if let nodeValue = node() as? InfoItemNode {
let makeLayout = nodeValue.asyncLayout() let makeLayout = nodeValue.asyncLayout()
async { async {
@ -102,20 +109,24 @@ class PermissionInfoItemListItem: PermissionInfoItem, ItemListItem {
private let titleFont = Font.semibold(17.0) private let titleFont = Font.semibold(17.0)
private let textFont = Font.regular(16.0) private let textFont = Font.regular(16.0)
private let textBoldFont = Font.semibold(16.0)
private let badgeFont = Font.regular(15.0) private let badgeFont = Font.regular(15.0)
class PermissionInfoItemNode: ListViewItemNode { class InfoItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
private let closeButton: HighlightableButtonNode private let closeButton: HighlightableButtonNode
let badgeNode: ASImageNode private let badgeNode: ASImageNode
let labelNode: TextNode private let labelNode: TextNode
let titleNode: TextNode private let titleNode: TextNode
let textNode: TextNode private let textNode: TextNode
private var linkHighlightingNode: LinkHighlightingNode?
private var item: PermissionInfoItem? private let activateArea: AccessibilityAreaNode
private var item: InfoListItem?
override var canBeSelected: Bool { override var canBeSelected: Bool {
return false return false
@ -146,6 +157,9 @@ class PermissionInfoItemNode: ListViewItemNode {
self.textNode = TextNode() self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false self.textNode.isUserInteractionEnabled = false
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = UIAccessibilityTraitStaticText
self.closeButton = HighlightableButtonNode() self.closeButton = HighlightableButtonNode()
self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0)
self.closeButton.displaysAsynchronously = false self.closeButton.displaysAsynchronously = false
@ -156,12 +170,28 @@ class PermissionInfoItemNode: ListViewItemNode {
self.addSubnode(self.labelNode) self.addSubnode(self.labelNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
self.addSubnode(self.activateArea)
self.addSubnode(self.closeButton) self.addSubnode(self.closeButton)
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside)
} }
func asyncLayout() -> (_ item: PermissionInfoItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) { override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
func asyncLayout() -> (_ item: InfoListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) {
let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeTextLayout = TextNode.asyncLayout(self.textNode)
@ -204,35 +234,31 @@ class PermissionInfoItemNode: ListViewItemNode {
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
} }
let title: String let attributedText: NSAttributedString
let text: String switch item.text {
switch item.subject { case let .plain(text):
case .contacts: attributedText = NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor)
title = item.strings.Contacts_PermissionsTitle case let .markdown(text):
text = item.strings.Contacts_PermissionsText attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in
case .notifications: return (TelegramTextAttributes.URL, contents)
switch item.type { }))
case .unreachable:
title = item.strings.Notifications_PermissionsUnreachableTitle
text = item.strings.Notifications_PermissionsUnreachableText
default:
title = item.strings.Notifications_PermissionsTitle
text = item.strings.Notifications_PermissionsText
}
default:
title = ""
text = ""
} }
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: badgeDiameter, height: badgeDiameter), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: badgeDiameter, height: badgeDiameter), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: titleLayout.size.height + textLayout.size.height + 36.0) let contentSize = CGSize(width: params.width, height: titleLayout.size.height + textLayout.size.height + 36.0)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.accessibilityLabel = "\(item.title)\n\(attributedText.string)"
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = strongSelf.accessibilityLabel
if let _ = updatedTheme { if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
@ -272,14 +298,7 @@ class PermissionInfoItemNode: ListViewItemNode {
bottomStripeInset = leftInset bottomStripeInset = leftInset
} }
if let item = strongSelf.item { strongSelf.closeButton.isHidden = item.closeAction == nil
switch (item.subject, item.type, item.suppressed) {
case (.contacts, _, false), (.notifications, .unreachable, _):
strongSelf.closeButton.isHidden = false
default:
strongSelf.closeButton.isHidden = true
}
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
@ -324,7 +343,72 @@ class PermissionInfoItemNode: ListViewItemNode {
@objc func closeButtonPressed() { @objc func closeButtonPressed() {
if let item = self.item { if let item = self.item {
item.close() item.closeAction?()
}
}
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let titleFrame = self.textNode.frame
if let item = self.item, titleFrame.contains(location) {
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}
}
}
default:
break
}
}
default:
break
}
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedStringKey(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
} }
} }
} }

View File

@ -543,7 +543,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
case .tap, .longTap: case .tap, .longTap:
if let item = self.item, let url = self.urlAtPoint(location) { if let item = self.item, let url = self.urlAtPoint(location) {
if case .longTap = gesture { if case .longTap = gesture {
item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url)) item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url), item.message.id)
} else if url == self.currentPrimaryUrl { } else if url == self.currentPrimaryUrl {
if !item.controllerInteraction.openMessage(item.message, .default) { if !item.controllerInteraction.openMessage(item.message, .default) {
item.controllerInteraction.openUrl(url, false, false) item.controllerInteraction.openUrl(url, false, false)

View File

@ -407,7 +407,7 @@ public final class MediaManager: NSObject {
} }
} }
func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType) { func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction = .playback(.play)) {
assert(Queue.mainQueue().isCurrent()) assert(Queue.mainQueue().isCurrent())
let inputData: Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings)?, NoError> let inputData: Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings)?, NoError>
if let (account, playlist) = playlist { if let (account, playlist) = playlist {
@ -444,7 +444,7 @@ public final class MediaManager: NSObject {
strongSelf.voiceMediaPlayer = nil strongSelf.voiceMediaPlayer = nil
} }
} }
voiceMediaPlayer.control(.playback(.play)) voiceMediaPlayer.control(control)
} else { } else {
strongSelf.voiceMediaPlayer = nil strongSelf.voiceMediaPlayer = nil
} }
@ -460,7 +460,7 @@ public final class MediaManager: NSObject {
strongSelf.musicMediaPlayer = nil strongSelf.musicMediaPlayer = nil
} }
} }
strongSelf.musicMediaPlayer?.control(.playback(.play)) strongSelf.musicMediaPlayer?.control(control)
} else { } else {
strongSelf.musicMediaPlayer = nil strongSelf.musicMediaPlayer = nil
} }

View File

@ -55,10 +55,11 @@ enum MediaPlayerPlayOnceWithSoundActionAtEnd {
case repeatIfNeeded case repeatIfNeeded
} }
enum MediaPlayerPlayOnceWithSoundSeek { enum MediaPlayerSeek {
case none case none
case start case start
case automatic case automatic
case timecode(Double)
} }
enum MediaPlayerStreaming { enum MediaPlayerStreaming {
@ -483,7 +484,7 @@ private final class MediaPlayerContext {
} }
} }
fileprivate func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek = .start) { fileprivate func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) {
assert(self.queue.isCurrent()) assert(self.queue.isCurrent())
if !self.enableSound { if !self.enableSound {
@ -506,9 +507,12 @@ private final class MediaPlayerContext {
} }
var timestamp: Double var timestamp: Double
if let loadedState = loadedState, seekToStart == .none { if case let .timecode(time) = seek {
timestamp = time
}
else if let loadedState = loadedState, case .none = seek {
timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
if let duration = currentDuration() { if let duration = self.currentDuration() {
if timestamp > duration - 2.0 { if timestamp > duration - 2.0 {
timestamp = 0.0 timestamp = 0.0
} }
@ -518,7 +522,10 @@ private final class MediaPlayerContext {
} }
self.seek(timestamp: timestamp, action: .play) self.seek(timestamp: timestamp, action: .play)
} else { } else {
if case .playing = self.state { if case let .timecode(time) = seek {
self.seek(timestamp: Double(time), action: .play)
}
else if case .playing = self.state {
} else { } else {
self.play() self.play()
} }
@ -551,7 +558,7 @@ private final class MediaPlayerContext {
self.playAndRecord = false self.playAndRecord = false
var timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) var timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
if let duration = currentDuration(), timestamp > duration - 2.0 { if let duration = self.currentDuration(), timestamp > duration - 2.0 {
timestamp = 0.0 timestamp = 0.0
} }
self.seek(timestamp: timestamp, action: .play) self.seek(timestamp: timestamp, action: .play)
@ -644,7 +651,7 @@ private final class MediaPlayerContext {
} }
case .paused: case .paused:
if !self.enableSound { if !self.enableSound {
self.playOnceWithSound(playAndRecord: false, seekToStart: .none) self.playOnceWithSound(playAndRecord: false, seek: .none)
} else { } else {
self.play() self.play()
} }
@ -969,10 +976,10 @@ final class MediaPlayer {
} }
} }
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek = .start) { func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) {
self.queue.async { self.queue.async {
if let context = self.contextRef?.takeUnretainedValue() { if let context = self.contextRef?.takeUnretainedValue() {
context.playOnceWithSound(playAndRecord: playAndRecord, seekToStart: seekToStart) context.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
} }
} }
} }
@ -1025,13 +1032,17 @@ final class MediaPlayer {
} }
} }
func seek(timestamp: Double) { func seek(timestamp: Double, play: Bool? = nil) {
self.queue.async { self.queue.async {
if let context = self.contextRef?.takeUnretainedValue() { if let context = self.contextRef?.takeUnretainedValue() {
if let play = play {
context.seek(timestamp: timestamp, action: play ? .play : .pause)
} else {
context.seek(timestamp: timestamp) context.seek(timestamp: timestamp)
} }
} }
} }
}
func setBaseRate(_ baseRate: Double) { func setBaseRate(_ baseRate: Double) {
self.queue.async { self.queue.async {

View File

@ -261,7 +261,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
self.player.seek(timestamp: timestamp) self.player.seek(timestamp: timestamp)
} }
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
assert(Queue.mainQueue().isCurrent()) assert(Queue.mainQueue().isCurrent())
let action = { [weak self] in let action = { [weak self] in
Queue.mainQueue().async { Queue.mainQueue().async {
@ -295,7 +295,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
}) })
} }
self.player.playOnceWithSound(playAndRecord: playAndRecord, seekToStart: seekToStart) self.player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
} }
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {

View File

@ -121,7 +121,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
case allAccounts(PresentationTheme, String, Bool) case allAccounts(PresentationTheme, String, Bool)
case accountsInfo(PresentationTheme, String) case accountsInfo(PresentationTheme, String)
case permissionInfo(PresentationTheme, PresentationStrings, AccessType) case permissionInfo(PresentationTheme, String, String, Bool)
case permissionEnable(PresentationTheme, String) case permissionEnable(PresentationTheme, String)
case messageHeader(PresentationTheme, String) case messageHeader(PresentationTheme, String)
@ -336,8 +336,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .permissionInfo(lhsTheme, lhsStrings, lhsAccessType): case let .permissionInfo(lhsTheme, lhsTitle, lhsText, lhsSuppressed):
if case let .permissionInfo(rhsTheme, rhsStrings, rhsAccessType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsAccessType == rhsAccessType { if case let .permissionInfo(rhsTheme, rhsTitle, rhsText, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSuppressed == rhsSuppressed {
return true return true
} else { } else {
return false return false
@ -569,8 +569,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
}, tag: self.tag) }, tag: self.tag)
case let .accountsInfo(theme, text): case let .accountsInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .permissionInfo(theme, strings, type): case let .permissionInfo(theme, title, text, suppressed):
return PermissionInfoItemListItem(theme: theme, strings: strings, subject: .notifications, type: type, style: .blocks, sectionId: self.section, suppressed: false, close: { return ItemListInfoItem(theme: theme, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: suppressed ? nil : {
arguments.suppressWarning() arguments.suppressWarning()
}) })
case let .permissionEnable(theme, text): case let .permissionEnable(theme, text):
@ -725,15 +725,25 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn
} }
if #available(iOSApplicationExtension 10.0, *) { if #available(iOSApplicationExtension 10.0, *) {
let title: String
let text: String
if case .unreachable = authorizationStatus {
title = presentationData.strings.Notifications_PermissionsUnreachableTitle
text = presentationData.strings.Notifications_PermissionsUnreachableText
} else {
title = presentationData.strings.Notifications_PermissionsTitle
text = presentationData.strings.Notifications_PermissionsText
}
switch (authorizationStatus, warningSuppressed) { switch (authorizationStatus, warningSuppressed) {
case (.denied, _): case (.denied, _):
entries.append(.permissionInfo(presentationData.theme, presentationData.strings, authorizationStatus)) entries.append(.permissionInfo(presentationData.theme, title, text, true))
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsAllowInSettings)) entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsAllowInSettings))
case (.unreachable, false): case (.unreachable, false):
entries.append(.permissionInfo(presentationData.theme, presentationData.strings, authorizationStatus)) entries.append(.permissionInfo(presentationData.theme, title, text, false))
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsOpenSettings)) entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsOpenSettings))
case (.notDetermined, _): case (.notDetermined, _):
entries.append(.permissionInfo(presentationData.theme, presentationData.strings, authorizationStatus)) entries.append(.permissionInfo(presentationData.theme, title, text, true))
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsAllow)) entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsAllow))
default: default:
break break

View File

@ -70,18 +70,22 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
} }
var stream = false var stream = false
var fromPlayingVideo = false var autoplayingVideo = false
var landscape = false var landscape = false
var timecode: Double? = nil
if case .stream = mode { switch mode {
case .stream:
stream = true stream = true
} case .automaticPlayback:
if case .automaticPlayback = mode { autoplayingVideo = true
fromPlayingVideo = true case .landscape:
} autoplayingVideo = true
if case .landscape = mode {
fromPlayingVideo = true
landscape = true landscape = true
case let .timecode(time):
timecode = time
default:
break
} }
if let (webPage, instantPageMedia) = instantPageMedia, let galleryMedia = galleryMedia { if let (webPage, instantPageMedia) = instantPageMedia, let galleryMedia = galleryMedia {
@ -93,7 +97,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
} }
} }
let gallery = InstantPageGalleryController(context: context, webPage: webPage, message: message, entries: instantPageMedia, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, landscape: landscape, replaceRootController: { [weak navigationController] controller, ready in let gallery = InstantPageGalleryController(context: context, webPage: webPage, message: message, entries: instantPageMedia, centralIndex: centralIndex, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, replaceRootController: { [weak navigationController] controller, ready in
if let navigationController = navigationController { if let navigationController = navigationController {
navigationController.replaceTopController(controller, animated: false, ready: ready) navigationController.replaceTopController(controller, animated: false, ready: ready)
} }
@ -124,7 +128,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
} }
#if DEBUG #if DEBUG
if ext == "mkv" { if ext == "mkv" {
let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: fromPlayingVideo, landscape: landscape, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
navigationController?.replaceTopController(controller, animated: false, ready: ready) navigationController?.replaceTopController(controller, animated: false, ready: ready)
}, baseNavigationController: navigationController, actionInteraction: actionInteraction) }, baseNavigationController: navigationController, actionInteraction: actionInteraction)
return .gallery(gallery) return .gallery(gallery)
@ -141,10 +145,10 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
let gallery = SecretMediaPreviewController(context: context, messageId: message.id) let gallery = SecretMediaPreviewController(context: context, messageId: message.id)
return .secretGallery(gallery) return .secretGallery(gallery)
} else { } else {
let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: fromPlayingVideo, landscape: landscape, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
navigationController?.replaceTopController(controller, animated: false, ready: ready) navigationController?.replaceTopController(controller, animated: false, ready: ready)
}, baseNavigationController: navigationController, actionInteraction: actionInteraction) }, baseNavigationController: navigationController, actionInteraction: actionInteraction)
gallery.temporaryDoNotWaitForReady = fromPlayingVideo gallery.temporaryDoNotWaitForReady = autoplayingVideo
return .gallery(gallery) return .gallery(gallery)
} }
} }
@ -251,6 +255,10 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
case let .audio(file): case let .audio(file):
let location: PeerMessagesPlaylistLocation let location: PeerMessagesPlaylistLocation
let playerType: MediaManagerPlayerType let playerType: MediaManagerPlayerType
var control = SharedMediaPlayerControlAction.playback(.play)
if case let .timecode(time) = mode {
control = .seek(time)
}
if (file.isVoice || file.isInstantVideo) && message.tags.contains(.voiceOrInstantVideo) { if (file.isVoice || file.isInstantVideo) && message.tags.contains(.voiceOrInstantVideo) {
if standalone { if standalone {
location = .recentActions(message) location = .recentActions(message)
@ -273,7 +281,7 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
} }
playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music playerType = (file.isVoice || file.isInstantVideo) ? .voice : .music
} }
context.sharedContext.mediaManager.setPlaylist((context.account, PeerMessagesMediaPlaylist(postbox: context.account.postbox, network: context.account.network, location: location)), type: playerType) context.sharedContext.mediaManager.setPlaylist((context.account, PeerMessagesMediaPlaylist(postbox: context.account.postbox, network: context.account.network, location: location)), type: playerType, control: control)
return true return true
case let .gallery(gallery): case let .gallery(gallery):
dismissInput() dismissInput()
@ -312,45 +320,6 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
} }
let controller = deviceContactInfoController(context: context, subject: .vcard(peer, nil, contactData)) let controller = deviceContactInfoController(context: context, subject: .vcard(peer, nil, contactData))
navigationController?.pushViewController(controller) navigationController?.pushViewController(controller)
guard let peer = peer else {
return
}
/*let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationTheme: presentationData.theme)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
var items: [ActionSheetItem] = []
if let peerId = contact.peerId {
items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_SendMessage, action: {
dismissAction()
openPeer(peer, .chat(textInputState: nil, messageId: nil))
}))
if let isContact = isContact, !isContact {
items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_AddContact, action: {
dismissAction()
let _ = addContactPeerInteractively(account: account, peerId: peerId, phone: contact.phoneNumber).start()
}))
}
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: {
dismissAction()
callPeer(peerId)
}))
}
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: {
dismissAction()
account.telegramApplicationcontext.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(contact.phoneNumber).replacingOccurrences(of: " ", with: ""))")
}))
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
dismissInput()
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))*/
}) })
return true return true
} }

View File

@ -60,7 +60,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec
return nil return nil
}, presentGlobalOverlayController: { _, _ in }, presentGlobalOverlayController: { _, _ in
}, callPeer: { _ in }, callPeer: { _ in
}, longTap: { _ in }, longTap: { _, _ in
}, openCheckoutOrReceipt: { _ in }, openCheckoutOrReceipt: { _ in
}, openSearch: { }, openSearch: {
}, setupReply: { _ in }, setupReply: { _ in
@ -73,6 +73,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec
}, requestSelectMessagePollOption: { _, _ in }, requestSelectMessagePollOption: { _, _ in
}, openAppStorePage: { }, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in }, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -205,7 +205,7 @@ public class PeerMediaCollectionController: TelegramController {
}, navigationController: { }, navigationController: {
return nil return nil
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in
}, longTap: { [weak self] content in }, longTap: { [weak self] content, _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.view.endEditing(true) strongSelf.view.endEditing(true)
switch content { switch content {
@ -254,6 +254,7 @@ public class PeerMediaCollectionController: TelegramController {
}, requestSelectMessagePollOption: { _, _ in }, requestSelectMessagePollOption: { _, _ in
}, openAppStorePage: { }, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in }, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -305,7 +305,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30)) self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30))
} }
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {

File diff suppressed because it is too large Load Diff

View File

@ -43,17 +43,20 @@ private struct SettingsItemArguments {
let openPassport: () -> Void let openPassport: () -> Void
let openWatch: () -> Void let openWatch: () -> Void
let openSupport: () -> Void let openSupport: () -> Void
let openFaq: () -> Void let openFaq: (String?) -> Void
let openEditing: () -> Void let openEditing: () -> Void
let displayCopyContextMenu: () -> Void let displayCopyContextMenu: () -> Void
let switchToAccount: (AccountRecordId) -> Void let switchToAccount: (AccountRecordId) -> Void
let addAccount: () -> Void let addAccount: () -> Void
let setAccountIdWithRevealedOptions: (AccountRecordId?, AccountRecordId?) -> Void let setAccountIdWithRevealedOptions: (AccountRecordId?, AccountRecordId?) -> Void
let removeAccount: (AccountRecordId) -> Void let removeAccount: (AccountRecordId) -> Void
let keepPhone: () -> Void
let openPhoneNumberChange: () -> Void
} }
private enum SettingsSection: Int32 { private enum SettingsSection: Int32 {
case info case info
case phone
case accounts case accounts
case proxy case proxy
case media case media
@ -67,6 +70,10 @@ private enum SettingsEntry: ItemListNodeEntry {
case setProfilePhoto(PresentationTheme, String) case setProfilePhoto(PresentationTheme, String)
case setUsername(PresentationTheme, String) case setUsername(PresentationTheme, String)
case phoneInfo(PresentationTheme, String, String)
case keepPhone(PresentationTheme, String)
case changePhone(PresentationTheme, String)
case account(Int, Account, PresentationTheme, PresentationStrings, Peer, Int32, Bool) case account(Int, Account, PresentationTheme, PresentationStrings, Peer, Int32, Bool)
case addAccount(PresentationTheme, String) case addAccount(PresentationTheme, String)
@ -91,6 +98,8 @@ private enum SettingsEntry: ItemListNodeEntry {
switch self { switch self {
case .userInfo, .setProfilePhoto, .setUsername: case .userInfo, .setProfilePhoto, .setUsername:
return SettingsSection.info.rawValue return SettingsSection.info.rawValue
case .phoneInfo, .keepPhone, .changePhone:
return SettingsSection.phone.rawValue
case .account, .addAccount: case .account, .addAccount:
return SettingsSection.accounts.rawValue return SettingsSection.accounts.rawValue
case .proxy: case .proxy:
@ -114,8 +123,14 @@ private enum SettingsEntry: ItemListNodeEntry {
return 1 return 1
case .setUsername: case .setUsername:
return 2 return 2
case .phoneInfo:
return 3
case .keepPhone:
return 4
case .changePhone:
return 5
case let .account(account): case let .account(account):
return 3 + Int32(account.0) return 6 + Int32(account.0)
case .addAccount: case .addAccount:
return 1002 return 1002
case .proxy: case .proxy:
@ -193,6 +208,24 @@ private enum SettingsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .phoneInfo(lhsTheme, lhsTitle, lhsText):
if case let .phoneInfo(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
case let .keepPhone(lhsTheme, lhsText):
if case let .keepPhone(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .changePhone(lhsTheme, lhsText):
if case let .changePhone(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .addAccount(lhsTheme, lhsText): case let .addAccount(lhsTheme, lhsText):
if case let .addAccount(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .addAccount(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@ -315,6 +348,20 @@ private enum SettingsEntry: ItemListNodeEntry {
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openUsername() arguments.openUsername()
}) })
case let .phoneInfo(theme, title, text):
return ItemListInfoItem(theme: theme, title: title, text: .markdown(text), style: .blocks, sectionId: self.section, linkAction: { action in
if case .tap = action {
arguments.openFaq("q-i-have-a-new-phone-number-what-do-i-do")
}
}, closeAction: nil)
case let .keepPhone(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.keepPhone()
})
case let .changePhone(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openPhoneNumberChange()
})
case let .account(_, account, theme, strings, peer, badgeCount, revealed): case let .account(_, account, theme, strings, peer, badgeCount, revealed):
var label: ItemListPeerItemLabel = .none var label: ItemListPeerItemLabel = .none
if badgeCount > 0 { if badgeCount > 0 {
@ -393,7 +440,7 @@ private enum SettingsEntry: ItemListNodeEntry {
}) })
case let .faq(theme, image, text): case let .faq(theme, image, text):
return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.openFaq() arguments.openFaq(nil)
}) })
} }
} }
@ -405,7 +452,7 @@ private struct SettingsState: Equatable {
var isSearching: Bool var isSearching: Bool
} }
private func settingsEntries(account: Account, presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, notifyExceptions: NotificationExceptionsList?, notificationsAuthorizationStatus: AccessType, notificationsWarningSuppressed: Bool, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, privacySettings: AccountPrivacySettings?, hasPassport: Bool, hasWatchApp: Bool, accountsAndPeers: [(Account, Peer, Int32)], inAppNotificationSettings: InAppNotificationSettings) -> [SettingsEntry] { private func settingsEntries(account: Account, presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, notifyExceptions: NotificationExceptionsList?, notificationsAuthorizationStatus: AccessType, notificationsWarningSuppressed: Bool, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, privacySettings: AccountPrivacySettings?, hasPassport: Bool, hasWatchApp: Bool, accountsAndPeers: [(Account, Peer, Int32)], inAppNotificationSettings: InAppNotificationSettings, displayPhoneNumberConfirmation: Bool) -> [SettingsEntry] {
var entries: [SettingsEntry] = [] var entries: [SettingsEntry] = []
if let peer = peerViewMainPeer(view) as? TelegramUser { if let peer = peerViewMainPeer(view) as? TelegramUser {
@ -418,6 +465,13 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
entries.append(.setUsername(presentationData.theme, presentationData.strings.Settings_SetUsername)) entries.append(.setUsername(presentationData.theme, presentationData.strings.Settings_SetUsername))
} }
if displayPhoneNumberConfirmation {
let phoneNumber = formatPhoneNumber(peer.phone ?? "")
entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).0, presentationData.strings.Settings_CheckPhoneNumberText))
entries.append(.keepPhone(presentationData.theme, presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0))
entries.append(.changePhone(presentationData.theme, presentationData.strings.Settings_ChangePhoneNumber))
}
if !accountsAndPeers.isEmpty { if !accountsAndPeers.isEmpty {
var index = 0 var index = 0
for (peerAccount, peer, badgeCount) in accountsAndPeers { for (peerAccount, peer, badgeCount) in accountsAndPeers {
@ -532,17 +586,6 @@ private final class SettingsControllerImpl: ItemListController<SettingsEntry>, S
} }
func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate) { func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate) {
guard let switchController = switchController else {
return
}
/*switch update {
case .dismiss:
switchController.dismiss()
case .present:
switchController.finishPresentation()
case let .update(progress):
switchController.update(progress)
}*/
} }
} }
@ -594,7 +637,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
let privacySettings = Promise<AccountPrivacySettings?>(nil) let privacySettings = Promise<AccountPrivacySettings?>(nil)
let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in let openFaq: (Promise<ResolvedUrl>, String?) -> Void = { resolvedUrl, customAnchor in
let _ = (contextValue.get() let _ = (contextValue.get()
|> deliverOnMainQueue |> deliverOnMainQueue
|> take(1)).start(next: { context in |> take(1)).start(next: { context in
@ -606,6 +649,10 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in |> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in
controller?.dismiss() controller?.dismiss()
var resolvedUrl = resolvedUrl
if case let .instantView(webPage, _) = resolvedUrl, let customAnchor = customAnchor {
resolvedUrl = .instantView(webPage, customAnchor)
}
openResolvedUrl(resolvedUrl, context: context, navigationController: getNavigationControllerImpl?(), openPeer: { peer, navigation in openResolvedUrl(resolvedUrl, context: context, navigationController: getNavigationControllerImpl?(), openPeer: { peer, navigation in
}, present: { controller, arguments in }, present: { controller, arguments in
pushControllerImpl?(controller) pushControllerImpl?(controller)
@ -622,6 +669,8 @@ public func settingsController(context: AccountContext, accountManager: AccountM
var switchToAccountImpl: ((AccountRecordId) -> Void)? var switchToAccountImpl: ((AccountRecordId) -> Void)?
let displayPhoneNumberConfirmation = ValuePromise<Bool>(false)
let arguments = SettingsItemArguments(accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { let arguments = SettingsItemArguments(accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: {
var updating = false var updating = false
updateState { updateState {
@ -744,7 +793,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [ presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: {
openFaq(resolvedUrlPromise) openFaq(resolvedUrlPromise, nil)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).start(next: { peerId in supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId { if let peerId = peerId {
@ -753,11 +802,10 @@ public func settingsController(context: AccountContext, accountManager: AccountM
})) }))
})]), nil) })]), nil)
}) })
}, openFaq: { }, openFaq: { anchor in
let resolvedUrlPromise = Promise<ResolvedUrl>() let resolvedUrlPromise = Promise<ResolvedUrl>()
resolvedUrlPromise.set(resolvedUrl) resolvedUrlPromise.set(resolvedUrl)
openFaq(resolvedUrlPromise, anchor)
openFaq(resolvedUrlPromise)
}, openEditing: { }, openEditing: {
let _ = (contextValue.get() let _ = (contextValue.get()
|> deliverOnMainQueue |> deliverOnMainQueue
@ -820,6 +868,19 @@ public func settingsController(context: AccountContext, accountManager: AccountM
]) ])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}) })
}, keepPhone: {
displayPhoneNumberConfirmation.set(false)
}, openPhoneNumberChange: {
let _ = (contextValue.get()
|> deliverOnMainQueue
|> take(1)).start(next: { context in
let _ = (context.account.postbox.transaction { transaction -> String in
return (transaction.getPeer(context.account.peerId) as? TelegramUser)?.phone ?? ""
}
|> deliverOnMainQueue).start(next: { phoneNumber in
pushControllerImpl?(ChangePhoneNumberIntroController(context: context, phoneNumber: formatPhoneNumber(phoneNumber)))
})
})
}) })
changeProfilePhotoImpl = { changeProfilePhotoImpl = {
@ -1050,7 +1111,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
return context.account.viewTracker.featuredStickerPacks() return context.account.viewTracker.featuredStickerPacks()
} }
let signal = combineLatest(queue: Queue.mainQueue(), contextValue.get(), updatedPresentationData, statePromise.get(), peerView, combineLatest(queue: Queue.mainQueue(), preferences, notifyExceptions.get(), notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), privacySettings.get()), combineLatest(featuredStickerPacks, archivedPacks.get()), combineLatest(hasPassport.get(), hasWatchApp.get()), accountsAndPeers.get()) let signal = combineLatest(queue: Queue.mainQueue(), contextValue.get(), updatedPresentationData, statePromise.get(), peerView, combineLatest(queue: Queue.mainQueue(), preferences, notifyExceptions.get(), notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), privacySettings.get(), displayPhoneNumberConfirmation.get()), combineLatest(featuredStickerPacks, archivedPacks.get()), combineLatest(hasPassport.get(), hasWatchApp.get()), accountsAndPeers.get())
|> map { context, presentationData, state, view, preferencesAndExceptions, featuredAndArchived, hasPassportAndWatch, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState<SettingsEntry>, SettingsEntry.ItemGenerationArguments)) in |> map { context, presentationData, state, view, preferencesAndExceptions, featuredAndArchived, hasPassportAndWatch, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState<SettingsEntry>, SettingsEntry.ItemGenerationArguments)) in
let proxySettings: ProxySettings = preferencesAndExceptions.0.entries[SharedDataKeys.proxySettings] as? ProxySettings ?? ProxySettings.defaultSettings let proxySettings: ProxySettings = preferencesAndExceptions.0.entries[SharedDataKeys.proxySettings] as? ProxySettings ?? ProxySettings.defaultSettings
let inAppNotificationSettings: InAppNotificationSettings = preferencesAndExceptions.0.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings let inAppNotificationSettings: InAppNotificationSettings = preferencesAndExceptions.0.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings
@ -1080,15 +1141,15 @@ public func settingsController(context: AccountContext, accountManager: AccountM
if value { if value {
setDisplayNavigationBarImpl?(false) setDisplayNavigationBarImpl?(false)
} }
}, presentController: { v, a in }, presentController: { c, a in
dismissInputImpl?() dismissInputImpl?()
presentControllerImpl?(v, a) presentControllerImpl?(c, a)
}, pushController: { v in }, pushController: { c in
pushControllerImpl?(v) pushControllerImpl?(c)
}, getNavigationController: getNavigationControllerImpl, exceptionsList: notifyExceptions.get(), archivedStickerPacks: archivedPacks.get(), privacySettings: privacySettings.get()) }, getNavigationController: getNavigationControllerImpl, exceptionsList: notifyExceptions.get(), archivedStickerPacks: archivedPacks.get(), privacySettings: privacySettings.get())
let (hasPassport, hasWatchApp) = hasPassportAndWatch let (hasPassport, hasWatchApp) = hasPassportAndWatch
let listState = ItemListNodeState(entries: settingsEntries(account: context.account, presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, notifyExceptions: preferencesAndExceptions.1, notificationsAuthorizationStatus: preferencesAndExceptions.2, notificationsWarningSuppressed: preferencesAndExceptions.3, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, privacySettings: preferencesAndExceptions.4, hasPassport: hasPassport, hasWatchApp: hasWatchApp, accountsAndPeers: accountsAndPeers.1, inAppNotificationSettings: inAppNotificationSettings), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)) let listState = ItemListNodeState(entries: settingsEntries(account: context.account, presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, notifyExceptions: preferencesAndExceptions.1, notificationsAuthorizationStatus: preferencesAndExceptions.2, notificationsWarningSuppressed: preferencesAndExceptions.3, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, privacySettings: preferencesAndExceptions.4, hasPassport: hasPassport, hasWatchApp: hasWatchApp, accountsAndPeers: accountsAndPeers.1, inAppNotificationSettings: inAppNotificationSettings, displayPhoneNumberConfirmation: preferencesAndExceptions.5), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up))
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }

View File

@ -672,14 +672,22 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode {
if let strongSelf = self { if let strongSelf = self {
switch mode { switch mode {
case .push: case .push:
if let controller = controller {
strongSelf.pushController(controller) strongSelf.pushController(controller)
}
case .modal: case .modal:
if let controller = controller {
strongSelf.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in strongSelf.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in
self?.cancel() self?.cancel()
})) }))
}
case .immediate: case .immediate:
if let controller = controller {
strongSelf.presentController(controller, nil) strongSelf.presentController(controller, nil)
} }
case .dismiss:
strongSelf.cancel()
}
} }
}) })
} }

View File

@ -138,6 +138,7 @@ enum SettingsSearchableItemPresentation {
case push case push
case modal case modal
case immediate case immediate
case dismiss
} }
struct SettingsSearchableItem { struct SettingsSearchableItem {
@ -146,7 +147,7 @@ struct SettingsSearchableItem {
let alternate: [String] let alternate: [String]
let icon: SettingsSearchableItemIcon let icon: SettingsSearchableItemIcon
let breadcrumbs: [String] let breadcrumbs: [String]
let present: (AccountContext, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void let present: (AccountContext, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void
} }
private func synonyms(_ string: String?) -> [String] { private func synonyms(_ string: String?) -> [String] {
@ -161,7 +162,7 @@ private func profileSearchableItems(context: AccountContext, canAddAccount: Bool
let icon: SettingsSearchableItemIcon = .profile let icon: SettingsSearchableItemIcon = .profile
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let presentProfileSettings: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController) -> Void, EditSettingsEntryTag?) -> Void = { context, present, itemTag in let presentProfileSettings: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, EditSettingsEntryTag?) -> Void = { context, present, itemTag in
let _ = openEditSettings(context: context, accountsAndPeers: activeAccountsAndPeers(context: context), focusOnItemTag: itemTag, presentController: { controller, _ in let _ = openEditSettings(context: context, accountsAndPeers: activeAccountsAndPeers(context: context), focusOnItemTag: itemTag, presentController: { controller, _ in
present(.immediate, controller) present(.immediate, controller)
}, pushController: { controller in }, pushController: { controller in
@ -211,7 +212,7 @@ private func callSearchableItems(context: AccountContext) -> [SettingsSearchable
let icon: SettingsSearchableItemIcon = .calls let icon: SettingsSearchableItemIcon = .calls
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let presentCallSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void = { context, present in let presentCallSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in
present(.push, CallListController(context: context, mode: .navigation)) present(.push, CallListController(context: context, mode: .navigation))
} }
@ -229,7 +230,7 @@ private func stickerSearchableItems(context: AccountContext, archivedStickerPack
let icon: SettingsSearchableItemIcon = .stickers let icon: SettingsSearchableItemIcon = .stickers
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let presentStickerSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, InstalledStickerPacksEntryTag?) -> Void = { context, present, itemTag in let presentStickerSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, InstalledStickerPacksEntryTag?) -> Void = { context, present, itemTag in
present(.push, installedStickerPacksController(context: context, mode: .general, archivedPacks: archivedStickerPacks, updatedPacks: { _ in }, focusOnItemTag: itemTag)) present(.push, installedStickerPacksController(context: context, mode: .general, archivedPacks: archivedStickerPacks, updatedPacks: { _ in }, focusOnItemTag: itemTag))
} }
@ -259,7 +260,7 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob
let icon: SettingsSearchableItemIcon = .notifications let icon: SettingsSearchableItemIcon = .notifications
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let presentNotificationSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, NotificationsAndSoundsEntryTag?) -> Void = { context, present, itemTag in let presentNotificationSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, NotificationsAndSoundsEntryTag?) -> Void = { context, present, itemTag in
present(.push, notificationsAndSoundsController(context: context, exceptionsList: exceptionsList, focusOnItemTag: itemTag)) present(.push, notificationsAndSoundsController(context: context, exceptionsList: exceptionsList, focusOnItemTag: itemTag))
} }
@ -302,7 +303,6 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob
} }
} }
} }
return (.users(users), .groups(groups), .channels(channels)) return (.users(users), .groups(groups), .channels(channels))
} }
@ -413,11 +413,11 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
let icon: SettingsSearchableItemIcon = .privacy let icon: SettingsSearchableItemIcon = .privacy
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let presentPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, PrivacyAndSecurityEntryTag?) -> Void = { context, present, itemTag in let presentPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, PrivacyAndSecurityEntryTag?) -> Void = { context, present, itemTag in
present(.push, privacyAndSecurityController(context: context, focusOnItemTag: itemTag)) present(.push, privacyAndSecurityController(context: context, focusOnItemTag: itemTag))
} }
let presentSelectivePrivacySettings: (AccountContext, SelectivePrivacySettingsKind, @escaping (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void = { context, kind, present in let presentSelectivePrivacySettings: (AccountContext, SelectivePrivacySettingsKind, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, kind, present in
let privacySignal: Signal<AccountPrivacySettings, NoError> let privacySignal: Signal<AccountPrivacySettings, NoError>
if let privacySettings = privacySettings { if let privacySettings = privacySettings {
privacySignal = .single(privacySettings) privacySignal = .single(privacySettings)
@ -463,7 +463,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
}) })
} }
let presentDataPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void = { context, present in let presentDataPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in
present(.push, dataPrivacyController(context: context)) present(.push, dataPrivacyController(context: context))
} }
@ -557,7 +557,7 @@ private func dataSearchableItems(context: AccountContext) -> [SettingsSearchable
let icon: SettingsSearchableItemIcon = .data let icon: SettingsSearchableItemIcon = .data
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let presentDataSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, DataAndStorageEntryTag?) -> Void = { context, present, itemTag in let presentDataSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, DataAndStorageEntryTag?) -> Void = { context, present, itemTag in
present(.push, dataAndStorageController(context: context, focusOnItemTag: itemTag)) present(.push, dataAndStorageController(context: context, focusOnItemTag: itemTag))
} }
@ -611,7 +611,7 @@ private func proxySearchableItems(context: AccountContext, servers: [ProxyServer
let icon: SettingsSearchableItemIcon = .proxy let icon: SettingsSearchableItemIcon = .proxy
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let presentProxySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void) -> Void = { context, present in let presentProxySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in
present(.push, proxySettingsController(context: context)) present(.push, proxySettingsController(context: context))
} }
@ -642,7 +642,7 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear
let icon: SettingsSearchableItemIcon = .appearance let icon: SettingsSearchableItemIcon = .appearance
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let presentAppearanceSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController) -> Void, ThemeSettingsEntryTag?) -> Void = { context, present, itemTag in let presentAppearanceSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, ThemeSettingsEntryTag?) -> Void = { context, present, itemTag in
present(.push, themeSettingsController(context: context, focusOnItemTag: itemTag)) present(.push, themeSettingsController(context: context, focusOnItemTag: itemTag))
} }
@ -676,6 +676,36 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear
] ]
} }
private func languageSearchableItems(context: AccountContext, localizations: [LocalizationInfo]) -> [SettingsSearchableItem] {
let icon: SettingsSearchableItemIcon = .language
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
let applyLocalization: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, String) -> Void = { context, present, languageCode in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: nil))
present(.immediate, controller)
let _ = (downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, languageCode: languageCode)
|> deliverOnMainQueue).start(completed: { [weak controller] in
controller?.dismiss()
present(.dismiss, nil)
})
}
var items: [SettingsSearchableItem] = []
items.append(SettingsSearchableItem(id: .language(0), title: strings.Settings_AppLanguage, alternate: synonyms(strings.SettingsSearch_Synonyms_AppLanguage), icon: icon, breadcrumbs: [], present: { context, _, present in
present(.push, LocalizationListController(context: context))
}))
var index: Int32 = 1
for localization in localizations {
items.append(SettingsSearchableItem(id: .language(index), title: localization.localizedTitle, alternate: [localization.title], icon: icon, breadcrumbs: [strings.Settings_AppLanguage], present: { context, _, present in
applyLocalization(context, present, localization.languageCode)
}))
index += 1
}
return items
}
func settingsSearchableItems(context: AccountContext, notificationExceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>) -> Signal<[SettingsSearchableItem], NoError> { func settingsSearchableItems(context: AccountContext, notificationExceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>) -> Signal<[SettingsSearchableItem], NoError> {
let watchAppInstalled = (context.watchManager?.watchAppInstalled ?? .single(false)) let watchAppInstalled = (context.watchManager?.watchAppInstalled ?? .single(false))
|> take(1) |> take(1)
@ -716,8 +746,40 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
return settings.servers return settings.servers
} }
return combineLatest(watchAppInstalled, canAddAccount, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings) let localizationPreferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState]))
|> map { watchAppInstalled, canAddAccount, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings in let localizations = context.account.postbox.combinedView(keys: [localizationPreferencesKey])
|> map { view -> [LocalizationInfo] in
if let localizationListState = (view.views[localizationPreferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState] as? LocalizationListState, !localizationListState.availableOfficialLocalizations.isEmpty {
var existingIds = Set<String>()
let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) })
var localizationItems: [LocalizationInfo] = []
if !availableSavedLocalizations.isEmpty {
for info in availableSavedLocalizations {
if existingIds.contains(info.languageCode) {
continue
}
existingIds.insert(info.languageCode)
localizationItems.append(info)
}
}
for info in localizationListState.availableOfficialLocalizations {
if existingIds.contains(info.languageCode) {
continue
}
existingIds.insert(info.languageCode)
localizationItems.append(info)
}
return localizationItems
} else {
return []
}
}
return combineLatest(watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings)
|> map { watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings in
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
var allItems: [SettingsSearchableItem] = [] var allItems: [SettingsSearchableItem] = []
@ -751,10 +813,8 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
let appearanceItems = appearanceSearchableItems(context: context) let appearanceItems = appearanceSearchableItems(context: context)
allItems.append(contentsOf: appearanceItems) allItems.append(contentsOf: appearanceItems)
let language = SettingsSearchableItem(id: .language(0), title: strings.Settings_AppLanguage, alternate: synonyms(strings.SettingsSearch_Synonyms_AppLanguage), icon: .language, breadcrumbs: [], present: { context, _, present in let languageItems = languageSearchableItems(context: context, localizations: localizations)
present(.push, LocalizationListController(context: context)) allItems.append(contentsOf: languageItems)
})
allItems.append(language)
if watchAppInstalled { if watchAppInstalled {
let watch = SettingsSearchableItem(id: .watch(0), title: strings.Settings_AppleWatch, alternate: synonyms(strings.SettingsSearch_Synonyms_Watch), icon: .watch, breadcrumbs: [], present: { context, _, present in let watch = SettingsSearchableItem(id: .watch(0), title: strings.Settings_AppleWatch, alternate: synonyms(strings.SettingsSearch_Synonyms_Watch), icon: .watch, breadcrumbs: [], present: { context, _, present in

View File

@ -402,6 +402,7 @@ final class SharedMediaPlayer {
private var playbackItem: SharedMediaPlaybackItem? private var playbackItem: SharedMediaPlaybackItem?
private var currentPlayedToEnd = false private var currentPlayedToEnd = false
private var scheduledPlaybackAction: SharedMediaPlayerPlaybackControlAction? private var scheduledPlaybackAction: SharedMediaPlayerPlaybackControlAction?
private var scheduledStartTime: Double?
private let markItemAsPlayedDisposable = MetaDisposable() private let markItemAsPlayedDisposable = MetaDisposable()
@ -507,11 +508,19 @@ final class SharedMediaPlayer {
if let scheduledPlaybackAction = strongSelf.scheduledPlaybackAction { if let scheduledPlaybackAction = strongSelf.scheduledPlaybackAction {
strongSelf.scheduledPlaybackAction = nil strongSelf.scheduledPlaybackAction = nil
let scheduledStartTime = strongSelf.scheduledStartTime
strongSelf.scheduledStartTime = nil
switch scheduledPlaybackAction { switch scheduledPlaybackAction {
case .play: case .play:
switch playbackItem { switch playbackItem {
case let .audio(player): case let .audio(player):
if let scheduledStartTime = scheduledStartTime {
player.seek(timestamp: scheduledStartTime)
player.play() player.play()
} else {
player.play()
}
case let .instantVideo(node): case let .instantVideo(node):
node.playOnceWithSound(playAndRecord: controlPlaybackWithProximity) node.playOnceWithSound(playAndRecord: controlPlaybackWithProximity)
} }
@ -654,6 +663,9 @@ final class SharedMediaPlayer {
case let .seek(timestamp): case let .seek(timestamp):
if let playbackItem = self.playbackItem { if let playbackItem = self.playbackItem {
playbackItem.seek(timestamp) playbackItem.seek(timestamp)
} else {
self.scheduledPlaybackAction = .play
self.scheduledStartTime = timestamp
} }
case let .setOrder(order): case let .setOrder(order):
self.playlist.setOrder(order) self.playlist.setOrder(order)

View File

@ -164,6 +164,20 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba
string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range) string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range)
case .Code, .Pre: case .Code, .Pre:
string.addAttribute(NSAttributedStringKey.font, value: fixedFont, range: range) string.addAttribute(NSAttributedStringKey.font, value: fixedFont, range: range)
case let .Custom(type):
if type == ApplicationSpecificEntityType.Timecode {
string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range)
}
if nsString == nil {
nsString = text as NSString
}
let text = nsString!.substring(with: range)
if let time = parseTimecodeString(text) {
string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode), value: TelegramTimecode(time: time, text: text), range: range)
}
}
default: default:
break break
} }

View File

@ -227,7 +227,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
//self.playerView.seek(toPosition: timestamp) //self.playerView.seek(toPosition: timestamp)
} }
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {

View File

@ -23,10 +23,21 @@ final class TelegramPeerMention {
} }
} }
final class TelegramTimecode {
let time: Double
let text: String
init(time: Double, text: String) {
self.time = time
self.text = text
}
}
struct TelegramTextAttributes { struct TelegramTextAttributes {
static let URL = "UrlAttributeT" static let URL = "UrlAttributeT"
static let PeerMention = "TelegramPeerMention" static let PeerMention = "TelegramPeerMention"
static let PeerTextMention = "TelegramPeerTextMention" static let PeerTextMention = "TelegramPeerTextMention"
static let BotCommand = "TelegramBotCommand" static let BotCommand = "TelegramBotCommand"
static let Hashtag = "TelegramHashtag" static let Hashtag = "TelegramHashtag"
static let Timecode = "TelegramTimecode"
} }

View File

@ -23,11 +23,12 @@ class UniversalVideoGalleryItem: GalleryItem {
let hideControls: Bool let hideControls: Bool
let fromPlayingVideo: Bool let fromPlayingVideo: Bool
let landscape: Bool let landscape: Bool
let timecode: Double?
let playbackCompleted: () -> Void let playbackCompleted: () -> Void
let performAction: (GalleryControllerInteractionTapAction) -> Void let performAction: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) { init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) {
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.content = content self.content = content
@ -39,6 +40,7 @@ class UniversalVideoGalleryItem: GalleryItem {
self.hideControls = hideControls self.hideControls = hideControls
self.fromPlayingVideo = fromPlayingVideo self.fromPlayingVideo = fromPlayingVideo
self.landscape = landscape self.landscape = landscape
self.timecode = timecode
self.playbackCompleted = playbackCompleted self.playbackCompleted = playbackCompleted
self.performAction = performAction self.performAction = performAction
self.openActionOptions = openActionOptions self.openActionOptions = openActionOptions
@ -195,8 +197,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
super.init() super.init()
self.scrubberView.seek = { [weak self] timestamp in self.scrubberView.seek = { [weak self] timecode in
self?.videoNode?.seek(timestamp) self?.videoNode?.seek(timecode)
} }
self.statusButtonNode.addSubnode(self.statusNode) self.statusButtonNode.addSubnode(self.statusNode)
@ -612,19 +614,34 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
} }
override func processAction(_ action: GalleryControllerItemNodeAction) {
guard let videoNode = self.videoNode else {
return
}
switch action {
case let .timecode(timecode):
videoNode.seek(timecode)
}
}
override func activateAsInitial() { override func activateAsInitial() {
if let videoNode = self.videoNode, self.isCentral { if let videoNode = self.videoNode, self.isCentral {
self.initiallyActivated = true self.initiallyActivated = true
var isAnimated = false var isAnimated = false
var seek = MediaPlayerSeek.start
if let item = self.item, let content = item.content as? NativeVideoContent { if let item = self.item, let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated isAnimated = content.fileReference.media.isAnimated
if let time = item.timecode {
seek = .timecode(time)
}
} }
if isAnimated { if isAnimated {
videoNode.seek(0.0) videoNode.seek(0.0)
videoNode.play() videoNode.play()
} else { } else {
videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop) videoNode.playOnceWithSound(playAndRecord: false, seek: seek, actionAtEnd: .stop)
} }
} }
} }
@ -979,18 +996,18 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if let fetchStatus = self.fetchStatus { if let fetchStatus = self.fetchStatus {
switch fetchStatus { switch fetchStatus {
case .Local: case .Local:
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: .stop) videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: .stop)
case .Remote: case .Remote:
if self.requiresDownload { if self.requiresDownload {
self.fetchControls?.fetch() self.fetchControls?.fetch()
} else { } else {
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: .stop) videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: .stop)
} }
case .Fetching: case .Fetching:
self.fetchControls?.cancel() self.fetchControls?.cancel()
} }
} else { } else {
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: .stop) videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: .stop)
} }
} }
} }

View File

@ -17,7 +17,7 @@ protocol UniversalVideoContentNode: class {
func togglePlayPause() func togglePlayPause()
func setSoundEnabled(_ value: Bool) func setSoundEnabled(_ value: Bool)
func seek(_ timestamp: Double) func seek(_ timestamp: Double)
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool)
@ -271,10 +271,10 @@ final class UniversalVideoNode: ASDisplayNode {
}) })
} }
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek = .start, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) { func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode { if let contentNode = contentNode {
contentNode.playOnceWithSound(playAndRecord: playAndRecord, seekToStart: seekToStart, actionAtEnd: actionAtEnd) contentNode.playOnceWithSound(playAndRecord: playAndRecord, seek: seek, actionAtEnd: actionAtEnd)
} }
}) })
} }

View File

@ -141,7 +141,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte
self.playerNode.seek(timestamp: timestamp) self.playerNode.seek(timestamp: timestamp)
} }
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {