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 */; };
09B4EE4F21A7B75D00847FA6 /* PermissionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.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 */; };
09B4EE6021AD4A0E00847FA6 /* InstantPageContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.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>"; };
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>"; };
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>"; };
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>"; };
@ -2922,7 +2922,6 @@
D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */,
D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */,
D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */,
09B4EE5521A8149C00847FA6 /* PermissionInfoItem.swift */,
);
name = Notifications;
sourceTree = "<group>";
@ -4153,6 +4152,7 @@
D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */,
D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */,
D0BFAE5A20AB35D200793CF2 /* IconSwitchNode.swift */,
09B4EE5521A8149C00847FA6 /* ItemListInfoItem.swift */,
);
name = Items;
sourceTree = "<group>";
@ -5879,7 +5879,7 @@
09749BC521F0E024008FDDE9 /* StickersChatInputPanelItem.swift in Sources */,
D0EC6DCF1EB9F58900EBF1C3 /* HorizontalStickerGridItem.swift in Sources */,
D0EC6DD01EB9F58900EBF1C3 /* HashtagChatInputContextPanelNode.swift in Sources */,
09B4EE5621A8149C00847FA6 /* PermissionInfoItem.swift in Sources */,
09B4EE5621A8149C00847FA6 /* ItemListInfoItem.swift in Sources */,
D0EC6DD11EB9F58900EBF1C3 /* HashtagChatInputPanelItem.swift in Sources */,
D0EC6DD21EB9F58900EBF1C3 /* MentionChatInputContextPanelNode.swift in Sources */,
D00701A22029F6D0006B9E34 /* TGMimeTypeMap.m in Sources */,

View File

@ -108,7 +108,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
break
case .ignore:
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
}
}
@ -301,15 +301,15 @@ final class ChatBotInfoItemNode: ListViewItemNode {
case .none, .ignore:
break
case let .url(url, _):
item.controllerInteraction.longTap(.url(url))
item.controllerInteraction.longTap(.url(url), nil)
case let .peerMention(peerId, mention):
item.controllerInteraction.longTap(.peerMention(peerId, mention))
item.controllerInteraction.longTap(.peerMention(peerId, mention), nil)
case let .textMention(name):
item.controllerInteraction.longTap(.mention(name))
item.controllerInteraction.longTap(.mention(name), nil)
case let .botCommand(command):
item.controllerInteraction.longTap(.command(command))
item.controllerInteraction.longTap(.command(command), nil)
case let .hashtag(_, hashtag):
item.controllerInteraction.longTap(.hashtag(hashtag))
item.controllerInteraction.longTap(.hashtag(hashtag), nil)
default:
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 {
switch action {
case let .url(url):
@ -1026,6 +1026,30 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
])])
strongSelf.chatDisplayNode.dismissInput()
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
@ -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
if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
@ -3287,7 +3329,25 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
return
}
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()
@ -3511,7 +3571,13 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false
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 command(String)
case hashtag(String)
case timecode(Double, String)
}
public enum ChatControllerInteractionOpenMessageMode {
@ -42,6 +43,7 @@ public enum ChatControllerInteractionOpenMessageMode {
case stream
case automaticPlayback
case landscape
case timecode(Double)
}
struct ChatInterfacePollActionState: Equatable {
@ -75,7 +77,7 @@ public final class ChatControllerInteraction {
let navigationController: () -> NavigationController?
let presentGlobalOverlayController: (ViewController, Any?) -> Void
let callPeer: (PeerId) -> Void
let longTap: (ChatControllerInteractionLongTapAction) -> Void
let longTap: (ChatControllerInteractionLongTapAction, MessageId?) -> Void
let openCheckoutOrReceipt: (MessageId) -> Void
let openSearch: () -> Void
let setupReply: (MessageId) -> Void
@ -87,6 +89,7 @@ public final class ChatControllerInteraction {
let requestSelectMessagePollOption: (MessageId, Data) -> Void
let openAppStorePage: () -> Void
let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
let seekToTimecode: (MessageId, Double) -> Void
let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void
@ -99,7 +102,7 @@ public final class ChatControllerInteraction {
var pollActionState: ChatInterfacePollActionState
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.openPeer = openPeer
self.openPeerMention = openPeerMention
@ -138,6 +141,7 @@ public final class ChatControllerInteraction {
self.requestSelectMessagePollOption = requestSelectMessagePollOption
self.openAppStorePage = openAppStorePage
self.displayMessageTooltip = displayMessageTooltip
self.seekToTimecode = seekToTimecode
self.requestMessageUpdate = requestMessageUpdate
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
}, presentController: { _, _ in }, navigationController: {
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
return false
}, navigateToFirstDateMessage: { _ in
@ -162,6 +166,7 @@ public final class ChatControllerInteraction {
}, requestSelectMessagePollOption: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -1488,43 +1488,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
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 {
if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
return inputPanelNode.isFocused

View File

@ -248,7 +248,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag]
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.Timecode]
for attribute in highlightedAttributes {
if let _ = attributes[NSAttributedStringKey(rawValue: attribute)] {
@ -308,10 +309,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
private func actionForAttributes(_ attributes: [NSAttributedStringKey: Any]) -> GalleryControllerInteractionTapAction? {
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)
} else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention)
@ -321,6 +318,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
return .botCommand(botCommand)
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
} else if let timecode = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
return .timecode(timecode.time, timecode.text)
} else {
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()
}
}

View File

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

View File

@ -257,7 +257,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
break
case .ignore:
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
}
}
@ -1727,6 +1727,37 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
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 {
@ -1734,6 +1765,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
}
case .longTap, .doubleTap:
if let item = self.item, self.backgroundNode.frame.contains(location) {
let messageId = item.message.id
var foundTapAction = false
var tapMessage: Message? = item.content.firstMessage
var selectAll = true
@ -1750,23 +1783,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
break
case let .url(url, _):
foundTapAction = true
item.controllerInteraction.longTap(.url(url))
item.controllerInteraction.longTap(.url(url), messageId)
break loop
case let .peerMention(peerId, mention):
foundTapAction = true
item.controllerInteraction.longTap(.peerMention(peerId, mention))
item.controllerInteraction.longTap(.peerMention(peerId, mention), messageId)
break loop
case let .textMention(name):
foundTapAction = true
item.controllerInteraction.longTap(.mention(name))
item.controllerInteraction.longTap(.mention(name), messageId)
break loop
case let .botCommand(command):
foundTapAction = true
item.controllerInteraction.longTap(.command(command))
item.controllerInteraction.longTap(.command(command), messageId)
break loop
case let .hashtag(_, hashtag):
foundTapAction = true
item.controllerInteraction.longTap(.hashtag(hashtag))
item.controllerInteraction.longTap(.hashtag(hashtag), messageId)
break loop
case .instantPage:
break
@ -1777,6 +1810,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
case .openMessage:
foundTapAction = false
break
case let .timecode(timecode, text):
foundTapAction = true
item.controllerInteraction.longTap(.timecode(timecode, text), messageId)
break loop
}
}
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 {
if let playMediaWithSound = contentNode.playMediaWithSound() {
return playMediaWithSound

View File

@ -709,7 +709,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
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()
}
}

View File

@ -504,7 +504,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
if 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 {
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 {
var isUnconsumed = false
for attribute in item.message.attributes {
@ -761,7 +761,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
}
}
return ({
return ({ _ in
if !self.infoBackgroundNode.alpha.isZero {
let _ = (item.context.sharedContext.mediaManager.globalMediaPlayerState
|> 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
if let file = self.media as? TelegramMediaFile, file.isAnimated {
isAnimated = true
@ -1224,25 +1224,30 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
}
if let videoNode = self.videoNode, let context = self.context, (self.automaticPlayback ?? false) && !isAnimated {
return ({
let _ = (context.sharedContext.mediaManager.globalMediaPlayerState
|> take(1)
|> deliverOnMainQueue).start(next: { playlistStateAndType in
var canPlay = true
if let (_, state, _) = playlistStateAndType {
switch state {
case let .state(state):
if case .playing = state.status.status {
canPlay = false
}
case .loading:
break
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
|> take(1)
|> deliverOnMainQueue).start(next: { playlistStateAndType in
var canPlay = true
if let (_, state, _) = playlistStateAndType {
switch state {
case let .state(state):
if case .playing = state.status.status {
canPlay = false
}
case .loading:
break
}
}
}
if canPlay {
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: actionAtEnd)
}
})
if canPlay {
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: actionAtEnd)
}
})
}
}, (self.playerStatus?.soundEnabled ?? false), false, false, self.badgeNode)
} else {
return nil

View File

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

View File

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

View File

@ -139,9 +139,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let attributedText: NSAttributedString
var messageEntities: [MessageTextEntity]?
var mediaDuration: Double? = nil
var isUnsupportedMedia = false
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
}
}
@ -154,7 +158,15 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
break
} 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
}
}
}
}
}
}
@ -166,8 +178,17 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
entities = cached.entities
} else {
entities = messageEntities
if entities == nil && mediaDuration != nil {
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
}
} else {
@ -372,6 +393,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return .botCommand(botCommand)
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
} else if let timecode = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
return .timecode(timecode.time, timecode.text)
} else {
return .none
}
@ -391,7 +414,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.Timecode
]
for name in possibleNames {
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)
}
override func playMediaWithSound() -> (() -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return self.contentNode.playMediaWithSound()
}

View File

@ -220,133 +220,156 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, presentController: { _, _ in
}, navigationController: { [weak self] in
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 {
switch action {
case let .url(url):
var cleanUrl = url
let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
var canAddToReadingList = true
let mailtoString = "mailto:"
let telString = "tel:"
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
if cleanUrl.hasPrefix(mailtoString) {
canAddToReadingList = false
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
} else if cleanUrl.hasPrefix(telString) {
canAddToReadingList = false
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
openText = strongSelf.presentationData.strings.Conversation_Call
} else if canOpenIn {
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
}
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: cleanUrl))
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.openUrl(url)
case let .url(url):
var cleanUrl = url
let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
var canAddToReadingList = true
let mailtoString = "mailto:"
let telString = "tel:"
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
if cleanUrl.hasPrefix(mailtoString) {
canAddToReadingList = false
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
} else if cleanUrl.hasPrefix(telString) {
canAddToReadingList = false
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
openText = strongSelf.presentationData.strings.Conversation_Call
} else if canOpenIn {
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
}
}))
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = cleanUrl
}))
if canAddToReadingList {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let link = URL(string: url) {
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
}
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
case let .peerMention(peerId, mention):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
if !mention.isEmpty {
items.append(ActionSheetTextItem(title: mention))
}
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.openPeer(peerId: peerId, peer: nil)
}
}))
if !mention.isEmpty {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
case let .mention(mention):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: mention),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: cleanUrl))
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.openPeerMention(mention)
strongSelf.openUrl(url)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
}))
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
})
]), ActionSheetItemGroup(items: [
UIPasteboard.general.string = cleanUrl
}))
if canAddToReadingList {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let link = URL(string: url) {
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
}
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
case let .command(command):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: command),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = command
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
case let .hashtag(hashtag):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: hashtag),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
])])
strongSelf.presentController(actionSheet, nil)
case let .peerMention(peerId, mention):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
if !mention.isEmpty {
items.append(ActionSheetTextItem(title: mention))
}
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
let searchController = HashtagSearchController(context: strongSelf.context, peer: strongSelf.peer, query: hashtag)
strongSelf.pushController(searchController)
strongSelf.openPeer(peerId: peerId, peer: nil)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = hashtag
})
]), ActionSheetItemGroup(items: [
}))
if !mention.isEmpty {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
case let .mention(mention):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: mention),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.openPeerMention(mention)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
strongSelf.presentController(actionSheet, nil)
case let .command(command):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: command),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = command
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.presentController(actionSheet, nil)
case let .hashtag(hashtag):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: hashtag),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
let searchController = HashtagSearchController(context: strongSelf.context, peer: strongSelf.peer, query: hashtag)
strongSelf.pushController(searchController)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = hashtag
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
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
@ -364,6 +387,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
strongSelf.context.sharedContext.applicationBindings.openAppStorePage()
}
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

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

View File

@ -131,7 +131,7 @@ private func galleryMessageCaptionText(_ message: Message) -> String {
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 {
case let .MessageEntry(message, _, location, _, _):
if let (media, mediaImage) = mediaForMessage(message: message) {
@ -157,8 +157,14 @@ func galleryItemForEntry(context: AccountContext, presentationData: Presentation
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 {
if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" {
var pixelsCount: Int = 0
@ -194,7 +200,7 @@ func galleryItemForEntry(context: AccountContext, presentationData: Presentation
}
}
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 {
return nil
}
@ -251,6 +257,11 @@ enum GalleryControllerInteractionTapAction {
case peerMention(PeerId, String)
case botCommand(String)
case hashtag(String?, String)
case timecode(Double, String)
}
public enum GalleryControllerItemNodeAction {
case timecode(Double)
}
final class GalleryControllerActionInteraction {
@ -298,6 +309,7 @@ class GalleryController: ViewController {
var temporaryDoNotWaitForReady = false
private let fromPlayingVideo: Bool
private let landscape: Bool
private let timecode: Double?
private let accountInUseDisposable = MetaDisposable()
private let disposable = MetaDisposable()
@ -323,7 +335,7 @@ class GalleryController: ViewController {
private var performAction: (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.source = source
self.replaceRootController = replaceRootController
@ -332,6 +344,7 @@ class GalleryController: ViewController {
self.streamVideos = streamSingleVideo
self.fromPlayingVideo = fromPlayingVideo
self.landscape = landscape
self.timecode = timecode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -438,7 +451,7 @@ class GalleryController: ViewController {
if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == strongSelf.centralEntryStableId {
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 {
centralItemIndex = items.count
}
@ -536,7 +549,10 @@ class GalleryController: ViewController {
performActionImpl = { [weak self] action in
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
if case .timecode = action {
} else {
strongSelf.dismiss(forceAway: false)
}
switch action {
case let .url(url, concealed):
strongSelf.actionInteraction?.openUrl(url, concealed)
@ -548,6 +564,8 @@ class GalleryController: ViewController {
strongSelf.actionInteraction?.openBotCommand(command)
case let .hashtag(peerName, hashtag):
strongSelf.actionInteraction?.openHashtag(peerName, hashtag)
case let .timecode(timecode, _):
strongSelf.galleryNode.pager.centralItemNode()?.processAction(.timecode(timecode))
}
}
}
@ -555,149 +573,171 @@ class GalleryController: ViewController {
openActionOptionsImpl = { [weak self] action in
if let strongSelf = self {
switch action {
case let .url(url, _):
var cleanUrl = url
var canAddToReadingList = true
let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
let mailtoString = "mailto:"
let telString = "tel:"
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
var phoneNumber: String?
if cleanUrl.hasPrefix(mailtoString) {
canAddToReadingList = false
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
} else if cleanUrl.hasPrefix(telString) {
canAddToReadingList = false
phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
cleanUrl = phoneNumber!
openText = strongSelf.presentationData.strings.UserInfo_PhoneCall
} else if canOpenIn {
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
}
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: cleanUrl))
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
if canOpenIn {
strongSelf.actionInteraction?.openUrlIn(url)
} else {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.openUrl(url, false)
}
case let .url(url, _):
var cleanUrl = url
var canAddToReadingList = true
let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
let mailtoString = "mailto:"
let telString = "tel:"
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
var phoneNumber: String?
if cleanUrl.hasPrefix(mailtoString) {
canAddToReadingList = false
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
} else if cleanUrl.hasPrefix(telString) {
canAddToReadingList = false
phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
cleanUrl = phoneNumber!
openText = strongSelf.presentationData.strings.UserInfo_PhoneCall
} else if canOpenIn {
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
}
}))
if let phoneNumber = phoneNumber {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: cleanUrl))
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
if canOpenIn {
strongSelf.actionInteraction?.openUrlIn(url)
} else {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.openUrl(url, false)
}
}
}))
if let phoneNumber = phoneNumber {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.addContact(phoneNumber)
}
}))
}
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = cleanUrl
}))
if canAddToReadingList {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let link = URL(string: url) {
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
}
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
case let .peerMention(peerId, mention):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
if !mention.isEmpty {
items.append(ActionSheetTextItem(title: mention))
}
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.addContact(phoneNumber)
strongSelf.actionInteraction?.openPeer(peerId)
}
}))
}
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = cleanUrl
}))
if canAddToReadingList {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let link = URL(string: url) {
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
}
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
case let .peerMention(peerId, mention):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
if !mention.isEmpty {
items.append(ActionSheetTextItem(title: mention))
}
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.openPeer(peerId)
if !mention.isEmpty {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
}))
}
}))
if !mention.isEmpty {
actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
case let .textMention(mention):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: mention),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.openPeerMention(mention)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
case let .botCommand(command):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: command))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
UIPasteboard.general.string = command
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
case let .textMention(mention):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: mention),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.openPeerMention(mention)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = mention
})
]), ActionSheetItemGroup(items: [
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
case let .botCommand(command):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: command))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = command
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
case let .hashtag(peerName, hashtag):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: hashtag),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.openHashtag(peerName, hashtag)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = hashtag
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
strongSelf.present(actionSheet, in: .window(.root))
case let .hashtag(peerName, hashtag):
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: hashtag),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.dismiss(forceAway: false)
strongSelf.actionInteraction?.openHashtag(peerName, hashtag)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = hashtag
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
])
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 centralItemIndex: Int?
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) {
if case let .MessageEntry(message, _, _, _, _) = entry, message.stableId == self.centralEntryStableId {
var isCentral = false
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
}
items.append(item)

View File

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

View File

@ -27,11 +27,28 @@ private let externalIdentifierDelimiterSet: CharacterSet = {
set.remove(".")
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 {
case command
case mention
case hashtag
case phoneNumber
case timecode
var type: EnabledEntityTypes {
switch self {
@ -41,6 +58,10 @@ private enum CurrentEntityType {
return .mention
case .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 url = EnabledEntityTypes(rawValue: 1 << 3)
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]
}
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) {
return
}
@ -76,15 +98,26 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType,
}
if !overlaps {
let entityType: MessageTextEntityType
switch type {
case .command:
entityType = .BotCommand
case .mention:
entityType = .Mention
case .hashtag:
entityType = .Hashtag
switch type {
case .command:
entityType = .BotCommand
case .mention:
entityType = .Mention
case .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))
}
} else {
entities.append(MessageTextEntity(range: indexRange, type: entityType))
}
entities.append(MessageTextEntity(range: indexRange, type: entityType))
}
}
@ -202,6 +235,8 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
}
currentEntity = nil
}
default:
break
}
}
}
@ -216,23 +251,34 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
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 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 {
if let scalar = UnicodeScalar(c) {
if scalar >= "0" && scalar <= "9" {
hasDigits = true
break loop
if !detectTimecodes || hasColons {
break loop
}
} else if scalar == ":" {
hasColons = true
if !detectPhoneNumbers || hasDigits {
break loop
}
}
}
}
}
if hasDigits {
if let phoneNumberDetector = phoneNumberDetector, enabledTypes.contains(.phoneNumber) {
if let phoneNumberDetector = phoneNumberDetector, detectPhoneNumbers {
let utf16 = text.utf16
phoneNumberDetector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in
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 upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text)
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)
var overlaps = false
for entity in resultEntities {
if entity.range.overlaps(indexRange) {
overlaps = true
break
}
}
if !overlaps {
resultEntities.append(MessageTextEntity(range: indexRange, type: .PhoneNumber))
}
commitEntity(utf16, .phoneNumber, lowerBound ..< upperBound, enabledTypes, &resultEntities)
}
}
}
})
}
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 {
@ -264,3 +343,25 @@ func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityType
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 storedStateDisposable: Disposable?
private var settings: InstantPagePresentationSettings?
private var settingsDisposable: Disposable?
@ -48,7 +49,7 @@ final class InstantPageController: ViewController {
}
})
self.settingsDisposable = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.instantPagePresentationSettings, ApplicationSpecificSharedDataKeys.presentationThemeSettings])
self.settingsDisposable = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.instantPagePresentationSettings, ApplicationSpecificSharedDataKeys.presentationThemeSettings])
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
if let strongSelf = self {
let settings: InstantPagePresentationSettings
@ -79,6 +80,7 @@ final class InstantPageController: ViewController {
deinit {
self.webpageDisposable?.dispose()
self.storedStateDisposable?.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
if let strongSelf = self {
strongSelf.controllerNode.updateWebPage(strongSelf.webPage, anchor: strongSelf.anchor, state: state)

View File

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

View File

@ -163,7 +163,7 @@ class InstantPageGalleryController: ViewController {
private var innerOpenUrl: (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.webPage = webPage
self.message = message

View File

@ -3,30 +3,37 @@ import Display
import AsyncDisplayKit
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 theme: PresentationTheme
let strings: PresentationStrings
let subject: DeviceAccessSubject
let type: AccessType
let title: String
let text: InfoListItemText
let style: ItemListStyle
let suppressed: Bool
let close: () -> Void
let linkAction: ((InfoListItemLinkAction) -> 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.strings = strings
self.subject = subject
self.type = type
self.title = title
self.text = text
self.style = style
self.suppressed = suppressed
self.close = close
self.linkAction = linkAction
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) {
async {
let node = PermissionInfoItemNode()
let node = InfoItemNode()
let (layout, apply) = node.asyncLayout()(self, params, nil)
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) {
Queue.mainQueue().async {
if let nodeValue = node() as? PermissionInfoItemNode {
if let nodeValue = node() as? InfoItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
@ -58,17 +65,17 @@ class PermissionInfoItem: ListViewItem {
}
}
class PermissionInfoItemListItem: PermissionInfoItem, ItemListItem {
class ItemListInfoItem: InfoListItem, ItemListItem {
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
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) {
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))
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) {
Queue.mainQueue().async {
if let nodeValue = node() as? PermissionInfoItemNode {
if let nodeValue = node() as? InfoItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
@ -102,20 +109,24 @@ class PermissionInfoItemListItem: PermissionInfoItem, ItemListItem {
private let titleFont = Font.semibold(17.0)
private let textFont = Font.regular(16.0)
private let textBoldFont = Font.semibold(16.0)
private let badgeFont = Font.regular(15.0)
class PermissionInfoItemNode: ListViewItemNode {
class InfoItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let closeButton: HighlightableButtonNode
let badgeNode: ASImageNode
let labelNode: TextNode
let titleNode: TextNode
let textNode: TextNode
private let badgeNode: ASImageNode
private let labelNode: TextNode
private let titleNode: 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 {
return false
@ -146,6 +157,9 @@ class PermissionInfoItemNode: ListViewItemNode {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = UIAccessibilityTraitStaticText
self.closeButton = HighlightableButtonNode()
self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0)
self.closeButton.displaysAsynchronously = false
@ -156,12 +170,28 @@ class PermissionInfoItemNode: ListViewItemNode {
self.addSubnode(self.labelNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.activateArea)
self.addSubnode(self.closeButton)
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 makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
@ -204,34 +234,30 @@ class PermissionInfoItemNode: ListViewItemNode {
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
}
let title: String
let text: String
switch item.subject {
case .contacts:
title = item.strings.Contacts_PermissionsTitle
text = item.strings.Contacts_PermissionsText
case .notifications:
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 attributedText: NSAttributedString
switch item.text {
case let .plain(text):
attributedText = NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor)
case let .markdown(text):
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
return (TelegramTextAttributes.URL, contents)
}))
}
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 (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 (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: 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)
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 {
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 {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
@ -272,14 +298,7 @@ class PermissionInfoItemNode: ListViewItemNode {
bottomStripeInset = leftInset
}
if let item = strongSelf.item {
switch (item.subject, item.type, item.suppressed) {
case (.contacts, _, false), (.notifications, .unreachable, _):
strongSelf.closeButton.isHidden = false
default:
strongSelf.closeButton.isHidden = true
}
}
strongSelf.closeButton.isHidden = item.closeAction == nil
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))
@ -324,7 +343,72 @@ class PermissionInfoItemNode: ListViewItemNode {
@objc func closeButtonPressed() {
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:
if let item = self.item, let url = self.urlAtPoint(location) {
if case .longTap = gesture {
item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url))
item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url), item.message.id)
} else if url == self.currentPrimaryUrl {
if !item.controllerInteraction.openMessage(item.message, .default) {
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())
let inputData: Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings)?, NoError>
if let (account, playlist) = playlist {
@ -444,7 +444,7 @@ public final class MediaManager: NSObject {
strongSelf.voiceMediaPlayer = nil
}
}
voiceMediaPlayer.control(.playback(.play))
voiceMediaPlayer.control(control)
} else {
strongSelf.voiceMediaPlayer = nil
}
@ -460,7 +460,7 @@ public final class MediaManager: NSObject {
strongSelf.musicMediaPlayer = nil
}
}
strongSelf.musicMediaPlayer?.control(.playback(.play))
strongSelf.musicMediaPlayer?.control(control)
} else {
strongSelf.musicMediaPlayer = nil
}

View File

@ -55,10 +55,11 @@ enum MediaPlayerPlayOnceWithSoundActionAtEnd {
case repeatIfNeeded
}
enum MediaPlayerPlayOnceWithSoundSeek {
enum MediaPlayerSeek {
case none
case start
case automatic
case timecode(Double)
}
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())
if !self.enableSound {
@ -506,9 +507,12 @@ private final class MediaPlayerContext {
}
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))
if let duration = currentDuration() {
if let duration = self.currentDuration() {
if timestamp > duration - 2.0 {
timestamp = 0.0
}
@ -518,7 +522,10 @@ private final class MediaPlayerContext {
}
self.seek(timestamp: timestamp, action: .play)
} 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 {
self.play()
}
@ -551,7 +558,7 @@ private final class MediaPlayerContext {
self.playAndRecord = false
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
}
self.seek(timestamp: timestamp, action: .play)
@ -644,7 +651,7 @@ private final class MediaPlayerContext {
}
case .paused:
if !self.enableSound {
self.playOnceWithSound(playAndRecord: false, seekToStart: .none)
self.playOnceWithSound(playAndRecord: false, seek: .none)
} else {
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 {
if let context = self.contextRef?.takeUnretainedValue() {
context.playOnceWithSound(playAndRecord: playAndRecord, seekToStart: seekToStart)
context.playOnceWithSound(playAndRecord: playAndRecord, seek: seek)
}
}
}
@ -1025,10 +1032,14 @@ final class MediaPlayer {
}
}
func seek(timestamp: Double) {
func seek(timestamp: Double, play: Bool? = nil) {
self.queue.async {
if let context = self.contextRef?.takeUnretainedValue() {
context.seek(timestamp: timestamp)
if let play = play {
context.seek(timestamp: timestamp, action: play ? .play : .pause)
} else {
context.seek(timestamp: timestamp)
}
}
}
}

View File

@ -261,7 +261,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
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())
let action = { [weak self] in
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) {

View File

@ -121,7 +121,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
case allAccounts(PresentationTheme, String, Bool)
case accountsInfo(PresentationTheme, String)
case permissionInfo(PresentationTheme, PresentationStrings, AccessType)
case permissionInfo(PresentationTheme, String, String, Bool)
case permissionEnable(PresentationTheme, String)
case messageHeader(PresentationTheme, String)
@ -336,8 +336,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
} else {
return false
}
case let .permissionInfo(lhsTheme, lhsStrings, lhsAccessType):
if case let .permissionInfo(rhsTheme, rhsStrings, rhsAccessType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsAccessType == rhsAccessType {
case let .permissionInfo(lhsTheme, lhsTitle, lhsText, lhsSuppressed):
if case let .permissionInfo(rhsTheme, rhsTitle, rhsText, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSuppressed == rhsSuppressed {
return true
} else {
return false
@ -569,8 +569,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
}, tag: self.tag)
case let .accountsInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .permissionInfo(theme, strings, type):
return PermissionInfoItemListItem(theme: theme, strings: strings, subject: .notifications, type: type, style: .blocks, sectionId: self.section, suppressed: false, close: {
case let .permissionInfo(theme, title, text, suppressed):
return ItemListInfoItem(theme: theme, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: suppressed ? nil : {
arguments.suppressWarning()
})
case let .permissionEnable(theme, text):
@ -725,15 +725,25 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn
}
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) {
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))
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))
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))
default:
break

View File

@ -70,18 +70,22 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
}
var stream = false
var fromPlayingVideo = false
var autoplayingVideo = false
var landscape = false
var timecode: Double? = nil
if case .stream = mode {
stream = true
}
if case .automaticPlayback = mode {
fromPlayingVideo = true
}
if case .landscape = mode {
fromPlayingVideo = true
landscape = true
switch mode {
case .stream:
stream = true
case .automaticPlayback:
autoplayingVideo = true
case .landscape:
autoplayingVideo = true
landscape = true
case let .timecode(time):
timecode = time
default:
break
}
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 {
navigationController.replaceTopController(controller, animated: false, ready: ready)
}
@ -124,7 +128,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
}
#if DEBUG
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)
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
return .gallery(gallery)
@ -141,10 +145,10 @@ private func chatMessageGalleryControllerData(context: AccountContext, message:
let gallery = SecretMediaPreviewController(context: context, messageId: message.id)
return .secretGallery(gallery)
} 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)
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
gallery.temporaryDoNotWaitForReady = fromPlayingVideo
gallery.temporaryDoNotWaitForReady = autoplayingVideo
return .gallery(gallery)
}
}
@ -251,6 +255,10 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
case let .audio(file):
let location: PeerMessagesPlaylistLocation
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 standalone {
location = .recentActions(message)
@ -273,7 +281,7 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
}
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
case let .gallery(gallery):
dismissInput()
@ -312,45 +320,6 @@ func openChatMessage(context: AccountContext, message: Message, standalone: Bool
}
let controller = deviceContactInfoController(context: context, subject: .vcard(peer, nil, contactData))
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
}

View File

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

View File

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

View File

@ -305,7 +305,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
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) {

File diff suppressed because it is too large Load Diff

View File

@ -43,17 +43,20 @@ private struct SettingsItemArguments {
let openPassport: () -> Void
let openWatch: () -> Void
let openSupport: () -> Void
let openFaq: () -> Void
let openFaq: (String?) -> Void
let openEditing: () -> Void
let displayCopyContextMenu: () -> Void
let switchToAccount: (AccountRecordId) -> Void
let addAccount: () -> Void
let setAccountIdWithRevealedOptions: (AccountRecordId?, AccountRecordId?) -> Void
let removeAccount: (AccountRecordId) -> Void
let keepPhone: () -> Void
let openPhoneNumberChange: () -> Void
}
private enum SettingsSection: Int32 {
case info
case phone
case accounts
case proxy
case media
@ -67,6 +70,10 @@ private enum SettingsEntry: ItemListNodeEntry {
case setProfilePhoto(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 addAccount(PresentationTheme, String)
@ -91,6 +98,8 @@ private enum SettingsEntry: ItemListNodeEntry {
switch self {
case .userInfo, .setProfilePhoto, .setUsername:
return SettingsSection.info.rawValue
case .phoneInfo, .keepPhone, .changePhone:
return SettingsSection.phone.rawValue
case .account, .addAccount:
return SettingsSection.accounts.rawValue
case .proxy:
@ -114,8 +123,14 @@ private enum SettingsEntry: ItemListNodeEntry {
return 1
case .setUsername:
return 2
case .phoneInfo:
return 3
case .keepPhone:
return 4
case .changePhone:
return 5
case let .account(account):
return 3 + Int32(account.0)
return 6 + Int32(account.0)
case .addAccount:
return 1002
case .proxy:
@ -193,6 +208,24 @@ private enum SettingsEntry: ItemListNodeEntry {
} else {
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):
if case let .addAccount(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
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: {
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):
var label: ItemListPeerItemLabel = .none
if badgeCount > 0 {
@ -393,7 +440,7 @@ private enum SettingsEntry: ItemListNodeEntry {
})
case let .faq(theme, image, text):
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
}
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] = []
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))
}
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 {
var index = 0
for (peerAccount, peer, badgeCount) in accountsAndPeers {
@ -532,17 +586,6 @@ private final class SettingsControllerImpl: ItemListController<SettingsEntry>, S
}
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 openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in
let openFaq: (Promise<ResolvedUrl>, String?) -> Void = { resolvedUrl, customAnchor in
let _ = (contextValue.get()
|> deliverOnMainQueue
|> take(1)).start(next: { context in
@ -605,7 +648,11 @@ public func settingsController(context: AccountContext, accountManager: AccountM
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in
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
}, present: { controller, arguments in
pushControllerImpl?(controller)
@ -622,6 +669,8 @@ public func settingsController(context: AccountContext, accountManager: AccountM
var switchToAccountImpl: ((AccountRecordId) -> Void)?
let displayPhoneNumberConfirmation = ValuePromise<Bool>(false)
let arguments = SettingsItemArguments(accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: {
var updating = false
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: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: {
openFaq(resolvedUrlPromise)
openFaq(resolvedUrlPromise, nil)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId {
@ -753,11 +802,10 @@ public func settingsController(context: AccountContext, accountManager: AccountM
}))
})]), nil)
})
}, openFaq: {
}, openFaq: { anchor in
let resolvedUrlPromise = Promise<ResolvedUrl>()
resolvedUrlPromise.set(resolvedUrl)
openFaq(resolvedUrlPromise)
openFaq(resolvedUrlPromise, anchor)
}, openEditing: {
let _ = (contextValue.get()
|> deliverOnMainQueue
@ -820,6 +868,19 @@ public func settingsController(context: AccountContext, accountManager: AccountM
])
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 = {
@ -1050,7 +1111,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
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
let proxySettings: ProxySettings = preferencesAndExceptions.0.entries[SharedDataKeys.proxySettings] as? ProxySettings ?? ProxySettings.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 {
setDisplayNavigationBarImpl?(false)
}
}, presentController: { v, a in
}, presentController: { c, a in
dismissInputImpl?()
presentControllerImpl?(v, a)
}, pushController: { v in
pushControllerImpl?(v)
presentControllerImpl?(c, a)
}, pushController: { c in
pushControllerImpl?(c)
}, getNavigationController: getNavigationControllerImpl, exceptionsList: notifyExceptions.get(), archivedStickerPacks: archivedPacks.get(), privacySettings: privacySettings.get())
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))
}

View File

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

View File

@ -138,6 +138,7 @@ enum SettingsSearchableItemPresentation {
case push
case modal
case immediate
case dismiss
}
struct SettingsSearchableItem {
@ -146,7 +147,7 @@ struct SettingsSearchableItem {
let alternate: [String]
let icon: SettingsSearchableItemIcon
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] {
@ -161,7 +162,7 @@ private func profileSearchableItems(context: AccountContext, canAddAccount: Bool
let icon: SettingsSearchableItemIcon = .profile
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
present(.immediate, controller)
}, pushController: { controller in
@ -211,7 +212,7 @@ private func callSearchableItems(context: AccountContext) -> [SettingsSearchable
let icon: SettingsSearchableItemIcon = .calls
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))
}
@ -229,7 +230,7 @@ private func stickerSearchableItems(context: AccountContext, archivedStickerPack
let icon: SettingsSearchableItemIcon = .stickers
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))
}
@ -259,7 +260,7 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob
let icon: SettingsSearchableItemIcon = .notifications
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))
}
@ -269,40 +270,39 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob
var channels:[PeerId : NotificationExceptionWrapper] = [:]
if let list = exceptionsList {
for (key, value) in list.settings {
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
switch value.muteState {
case .default:
switch value.messageSound {
case .default:
break
switch value.messageSound {
case .default:
break
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: peer)
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
}
}
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: peer)
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: peer)
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
}
}
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: peer)
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
}
}
}
}
}
return (.users(users), .groups(groups), .channels(channels))
}
@ -413,11 +413,11 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
let icon: SettingsSearchableItemIcon = .privacy
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))
}
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>
if let privacySettings = 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))
}
@ -557,7 +557,7 @@ private func dataSearchableItems(context: AccountContext) -> [SettingsSearchable
let icon: SettingsSearchableItemIcon = .data
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))
}
@ -611,7 +611,7 @@ private func proxySearchableItems(context: AccountContext, servers: [ProxyServer
let icon: SettingsSearchableItemIcon = .proxy
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))
}
@ -642,7 +642,7 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear
let icon: SettingsSearchableItemIcon = .appearance
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))
}
@ -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> {
let watchAppInstalled = (context.watchManager?.watchAppInstalled ?? .single(false))
|> take(1)
@ -716,8 +746,40 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
return settings.servers
}
return combineLatest(watchAppInstalled, canAddAccount, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings)
|> map { watchAppInstalled, canAddAccount, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings in
let localizationPreferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState]))
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
var allItems: [SettingsSearchableItem] = []
@ -751,10 +813,8 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
let appearanceItems = appearanceSearchableItems(context: context)
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
present(.push, LocalizationListController(context: context))
})
allItems.append(language)
let languageItems = languageSearchableItems(context: context, localizations: localizations)
allItems.append(contentsOf: languageItems)
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

View File

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

View File

@ -227,7 +227,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
//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) {

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 {
static let URL = "UrlAttributeT"
static let PeerMention = "TelegramPeerMention"
static let PeerTextMention = "TelegramPeerTextMention"
static let BotCommand = "TelegramBotCommand"
static let Hashtag = "TelegramHashtag"
static let Timecode = "TelegramTimecode"
}

View File

@ -23,11 +23,12 @@ class UniversalVideoGalleryItem: GalleryItem {
let hideControls: Bool
let fromPlayingVideo: Bool
let landscape: Bool
let timecode: Double?
let playbackCompleted: () -> Void
let performAction: (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.presentationData = presentationData
self.content = content
@ -39,6 +40,7 @@ class UniversalVideoGalleryItem: GalleryItem {
self.hideControls = hideControls
self.fromPlayingVideo = fromPlayingVideo
self.landscape = landscape
self.timecode = timecode
self.playbackCompleted = playbackCompleted
self.performAction = performAction
self.openActionOptions = openActionOptions
@ -195,8 +197,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
super.init()
self.scrubberView.seek = { [weak self] timestamp in
self?.videoNode?.seek(timestamp)
self.scrubberView.seek = { [weak self] timecode in
self?.videoNode?.seek(timecode)
}
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() {
if let videoNode = self.videoNode, self.isCentral {
self.initiallyActivated = true
var isAnimated = false
var seek = MediaPlayerSeek.start
if let item = self.item, let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
if let time = item.timecode {
seek = .timecode(time)
}
}
if isAnimated {
videoNode.seek(0.0)
videoNode.play()
} 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 {
switch fetchStatus {
case .Local:
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: .stop)
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: .stop)
case .Remote:
if self.requiresDownload {
self.fetchControls?.fetch()
} else {
videoNode.playOnceWithSound(playAndRecord: false, seekToStart: .none, actionAtEnd: .stop)
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: .stop)
}
case .Fetching:
self.fetchControls?.cancel()
}
} 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 setSoundEnabled(_ value: Bool)
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 continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
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
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)
}
func playOnceWithSound(playAndRecord: Bool, seekToStart: MediaPlayerPlayOnceWithSoundSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {